Controlling Nested Class Instantiation in C#

Posted by : on

Category : C#

Introduction

In a previous post: Data Driven Code Design I created an example using nested classes. For simplicity, that example only included code related to the posts’ subject. However, we sometimes encounter situations where we need nested classes within a class and want to restrict the creation of objects of those nested classes to the outer class exclusively.

While we might need access to the nested types from other parts of the code, we don’t want external code to be able to create instances of these public types. The enclosing class should have sole responsibility for their instantiation.

This post demonstrates several ways to achieve this. The solutions vary depending on our specific needs: the degree to which we want to restrict the construction of nested type instances, whether those types share common methods, and the language version we’re using.

Make the Nested Class Constructor Internal

The simplest initial approach is to change the public constructor’s visibility to internal. This is often sufficient if our code resides in a different assembly than the code using it. This prevents anyone outside our assembly from creating instances of the nested types.

While this post focuses on other solutions, I mention this one for completeness, as it’s the easiest to implement and frequently, limiting visibility to internal is enough. For situations where it’s not, I’ll describe some different ways the enclosing class can use the private constructor of its nested classes.

Before examining the problem, we must understand the differences in dependencies and visibility between classes with private constructors that are nested versus those that are not.

The only difference a nested class has compared to a non-nested class, regarding accessibility modifiers, is that nested classes can access the outer class’ instance’s private members, if they hold a reference to it. Any solution (other than reflection) must leverage this difference, create a private factory for the nested class, or access the static private members of the outer class (which, of course, don’t require an instance).

Use a Common Interface

Before doing all that though, a simpler solution exists if our nested classes have common methods, allowing them to implement the same interface. This works if we’re comfortable receiving objects of the interface type instead of the specific nested class types.

Suppose we have the following two classes that we want to nest inside an Outer class. We want to access instances of these classes through public members of the outer class but prevent external code from creating new instances.

public class Inner1
{

   private Inner1() { }
   public void Method1() => Console.WriteLine("Inner1 Method1");
   public void Method2() => Console.WriteLine("Inner1 Method2");
}

public class Inner2
{
   private Inner2() { }
   public void Method1() => Console.WriteLine("Inner2 Method1");
   public void Method2() => Console.WriteLine("Inner2 Method2");
}

A simple way to achieve this is to make the nested classes private and their constructors public. This allows the Outer class to access the constructors, while preventing external code from directly accessing the nested classes.

Then, by defining a common public interface, external code can interact with instances of the nested classes through that interface.

public class Outer
{
   public interface ICommonInterface
   {
      void Method1();
      void Method2();
   }

   public ICommonInterface InstanceOfInner1 = new Inner1();
   public ICommonInterface InstanceOfInner2 = new Inner2();

   private class Inner1 : ICommonInterface
   {

      public Inner1() { }
      public void Method1() => Console.WriteLine("Inner1 Method1");
      public void Method2() => Console.WriteLine("Inner1 Method2");
   }

   private class Inner2 : ICommonInterface
   {
      public Inner2() { }
      public void Method1() => Console.WriteLine("Inner2 Method1");
      public void Method2() => Console.WriteLine("Inner2 Method2");
   }
}

Now we can access the created nested classes’ instances like this:

Outer test = new Outer();

test.InstanceOfInner1.Method1();
test.InstanceOfInner2.Method1();

Outer.ICommonInterface Instance1 = test.InstanceOfInner1;

To allow external code to create instances of inner classes, we can provide a method to do so. This enables us to return ICommonInterface objects without revealing the underlying implementation type to the caller. For example:

public ICommonInterface GetInner1Instance()
{
   // code before initialization here
   return new Inner1();
}

While this addresses limiting access to nested type creation, let’s explore the core issue of this post: accessing public nested types with private constructors from code outside the Outer class.

Using a Delegate

One solution is to create a private delegate that points to the nested class’s constructor. This delegate cannot be assigned within the Outer class because it lacks access to the private members of its nested classes. However, the nested class can assign the delegate, provided it has a reference to an instance of the Outer class. Consider an Outer class with three nested classes, structured like this:

public class Outer
{
   public class Inner1
   {

      private Inner1() { }

      public void Method1() => Console.WriteLine("Inner1 Method1");
      public void Method2() => Console.WriteLine("Inner1 Method2");
   }

   public class Inner2
   {
      private Inner2() { }

      public void Method1() => Console.WriteLine("Inner2 Method1");
      public void Method2() => Console.WriteLine("Inner2 Method2");
   }
   
   public class Inner3
   {
      private Inner3() { }

      public void SomeOtherMethod1() => Console.WriteLine("Inner3 Some other method1");
      public void SomeOtherMethod2() => Console.WriteLine("Inner3 Some other method2");
   }
}

These nested classes cannot share a common interface, and we want to access their created instances from the Outer class without being able to create new instances of them directly. This is precisely why they are designed as public types with private constructors.

First, we create a private delegate within the Outer class and pass its instance as a parameter to the nested classes’ constructors:

public class Outer
{
   private Func<Outer, Inner1> _nestedInner1;
   private Func<Outer, Inner2> _nestedInner2;
   private Func<Outer, Inner3> _nestedInner3;
   
   public class Inner1
   {
      private Outer _outer;
      private Inner1(Outer outer) => _outer = outer;

      public void Method1() => Console.WriteLine("Inner1 Method1");
      public void Method2() => Console.WriteLine("Inner1 Method2");
   }

   public class Inner2
   {
      private Outer _outer;
      private Inner2(Outer outer) => _outer = outer;

      public void Method1() => Console.WriteLine("Inner2 Method1");
      public void Method2() => Console.WriteLine("Inner2 Method2");
   }
   
   public class Inner3
   {
      private Outer _outer;
      private Inner3(Outer outer) => _outer = outer;

      public void SomeOtherMethod1() => Console.WriteLine("Inner3 Some other method1");
      public void SomeOtherMethod2() => Console.WriteLine("Inner3 Some other method2");
   }
}

Next, we must ensure that the delegates are assigned during the instantiation of the Outer class. As previously mentioned, this cannot be done directly from the Outer class because it lacks access to the private constructors of the nested classes. Instead, the Outer class will call a static public method of the nested class. This method can access the (now also static) private delegates. The result of invoking these delegates is then assigned to public fields in the Outer class:

public class Outer
{
   public readonly Inner1 Inner1Instance;
   public readonly Inner2 Inner2Instance;
   public readonly Inner3 Inner3Instance;
   
   private static Func<Outer, Inner1>? _NestedInner1;
   private static Func<Outer, Inner2>? _NestedInner2;
   private static Func<Outer, Inner3>? _NestedInner3;

   private int exampleVariable;

   public Outer()
   {
      Inner1.Initialize();
      Inner1Instance = _NestedInner1!(this);
      Inner2.Initialize();
      Inner2Instance = _NestedInner2!(this);
      Inner3.Initialize();
      Inner3Instance = _NestedInner3!(this);
   }
   
   public class Inner1
   {
      private Outer _outer;
      private Inner1(Outer outer) => _outer = outer;

      public static void Initialize() => _NestedInner1 = value => new Inner1(value);

      public void Method1() => Console.WriteLine("Inner1 Method1");
      public void Method2() => Console.WriteLine("Inner1 Method2");
   }

   public class Inner2
   {
      private Outer _outer;
      private Inner2(Outer outer) => _outer = outer;
      
      public static void Initialize() => _NestedInner2 = value => new Inner2(value);

      public void Method1() => Console.WriteLine("Inner2 Method1");
      public void Method2() => Console.WriteLine("Inner2 Method2");
   }
   
   public class Inner3
   {
      private Outer _outer;
      private Inner3(Outer outer) => _outer = outer;
      
      public static void Initialize() => _NestedInner3 = value => new Inner3(value);

      public void SomeOtherMethod1() => Console.WriteLine("Inner3 Some other method1");
      public void SomeOtherMethod2() => Console.WriteLine("Inner3 Some other method2");
   }
}

We can now access instances of the nested classes like this:

Outer test = new Outer();

test.Inner1Instance.Method1();

The nested classes can access private instance members of the Outer class (e.g., exampleVariable) because they hold a reference to the outer class. Furthermore, no code outside the Outer class can create new instances of the nested classes directly. While external code could create new instances by calling something like:

Outer.Inner1.Initialize();

these instances are assigned to the private delegates of the Outer class, rendering them unusable externally.

This approach might seem extreme, and it’s certainly most useful in situations like my Data Driven Code Design example, particularly when we can’t place our code in a separate assembly to leverage the internal accessibility modifier. Indeed, it is rather specialized. However, let’s explore an even more complex scenario using generics.

Using Generics

The challenge with generics is that, prior to C# 11, interfaces could not have static members with different implementations in implementing classes (static abstract members). Consequently, we must take a more roundabout approach.

We’ll create a nested class within our nested class (yes, you read that right) that acts as a factory. This inner factory class will implement a generic interface that returns an instance of our original nested class, while explicitly implementing a method from that interface. This factory interface will be private and implemented inside our Outer class, preventing external access. Crucially, because this interface is implemented within a nested class within our nested class, it will have access to the private constructor of the original nested class.

In complex cases like this, code speaks louder than words, so let’s illustrate:

public class Outer
{
   public readonly Inner1 Inner1Instance;

   private interface IFactory<out T>
   {
      T GetInner();
   }

   private int _exampleValue;
   
   public Outer() => Inner1Instance = GetInner1Instance();

   private Inner1 GetInner1Instance()
   {
      IFactory<Inner1> factory = new Inner1.Inner1Factory(this);
      return factory.GetInner();
   }
   
   public class Inner1
   {
      private readonly Outer _outer;
      private Inner1(Outer outer) => _outer = outer;

      public void ShowOuterExampleValue() => Console.WriteLine(_outer._exampleValue);
      public void IncreaseOuterExampleValue() => _outer._exampleValue++;

      public class Inner1Factory(Outer outer) : IFactory<Inner1>
      {
         Inner1 IFactory<Inner1>.GetInner() => new(outer);
      }
   }
}

The Outer class constructor, calls the private GetInner1Instance method, which creates the public Inner1Factory object from the nested class. Because this object explicitly implements the IFactory<Inner1>.GetInner() method of the private IFactory<out T> interface, it cannot be used by code outside the Outer class.

This is a rather extreme example, and in most cases, it’s preferable to allow the creation of nested classes with public constructors rather than implementing something like this. At least then, any errors become the responsibility of those who create the nested class instances, and no one will have to decipher the complex code above.

Nevertheless, this approach is available if anyone chooses to use it. However, if you’re coding in C# 11 or later, the above can be simplified using static abstract members in interfaces.

Using Static Abstract Members (C# 11+)

C# 11 introduced Static abstract members in interfaces. We can leverage this feature to simplify the instantiation of public nested classes with private constructors.

We still need to create a generic private interface with a generic method that returns an instance of our nested class. However, this method can now be static abstract. This means it can be implemented as a static method within our nested class and called statically on the nested class type, eliminating the need for a separate factory class instantiation.

This pattern is a form of the Curiously Recurring Template Pattern (CRTP), which I discuss in more detail in my post: Curiously Recurring Template Pattern in C#. It represents a form of static polymorphism in C#, as the method belongs to the type, not an instance.

Here’s the code:

public class Outer
{
   public readonly Inner1 Inner1Instance;
   
   private int _exampleValue;

   public Outer() => Inner1Instance = GetInnerInstance<Inner1>();
   
   private interface IFactory<out T> where T: IFactory<T>
   {
      public static abstract T GetInner(Outer outer);
   }

   private T GetInnerInstance<T>() where T : IFactory<T> => T.GetInner(this);

   public class Inner1 : IFactory<Inner1>
   {
      private readonly Outer _outer;
      private Inner1(Outer outer) => _outer = outer;

      public void ShowOuterExampleValue() => Console.WriteLine(_outer._exampleValue);
      public void IncreaseOuterExampleValue() => _outer._exampleValue++;
      
      static Inner1 IFactory<Inner1>.GetInner(Outer outer) => new(outer);
   }
}

This approach is significantly simpler than the previous example. The only additional code required within each nested class is a single line: the implementation of the interface method that returns a new instance of the type.

Conclusion

The solutions presented for creating public nested classes that can only be instantiated by their enclosing class should be used with extreme caution in production code. The best approach is typically to use a separate assembly and declare the nested constructors internal. Even if this isn’t feasible, and you’re not the sole developer working with this code, it’s better to explicitly communicate that nested class object creation should be exclusively handled by the Outer class. This is preferable to having other programmers struggle to understand the complex code presented here.

In any case, if you encounter a niche scenario where you absolutely must prevent the creation of nested class instances within the same assembly, the code provided should be helpful. Thank you for reading, and if you have any questions or comments you can use the comments section, or contact me directly via the contact form or by email. Also, if you don’t want to miss any of the new blog posts, you can subscribe to my newsletter or the RSS feed.


Follow me: