Cristiano Santos
Cristiano Santos

Reputation: 2137

Allow subclass instantiation only on the assembly of the superclass in C#

Imagine the following scenario in a Xamarin solution:

Assembly A (PCL):

public abstract class MyBaseClass
{
    public MyBaseClass()
    {
        [...]
    }
    [...]
}

Assembly B (3rd Party Library):

public class SomeLibClass
{
    [...]
    public void MethodThatCreatesClass(Type classType){
        [...]
        //I want to allow this to work
        var obj = Activator.CreateInstance(classType);
        [...]
    }
    [...]
}

Assembly C (Main project):

public class ClassImplA:MyBaseClass{
    [...]
}

public class ClassImplA:MyBaseClass{
    [...]
}

public class TheProblem{
    public void AnExample(){
        [...]
        //I want to block these instantiations for this Assembly and any other with subclasses of MyBaseClass
        var obj1 = new ClassImplA()
        var obj2 = new ClassImplB()
        [...]
    }
}

How can I prevent the subclasses from being instantiated on their own assembly and allow them only on the super class and the 3rd Party Library (using Activator.CreateInstance)?

Attempt 1

I though I could make the base class with an internal constructor but then, I saw how silly that was because the subclasses wouldn't be able to inherit the constructor and so they wouldn't be able to inherit from the superclass.

Attempt 2

I tried using Assembly.GetCallingAssembly on the base class, but that is not available on PCL projects. The solution I found was to call it through reflection but it also didn't work since the result of that on the base class would be the Assembly C for both cases (and I think that's because who calls the constructor of MyBaseClass is indeed the default constructors of ClassImplA and ClassImplB for both cases).

Any other idea of how to do this? Or am I missing something here?

Update

The idea is to have the the PCL assembly abstract the main project (and some other projects) from offline synchronization.

Given that, my PCL uses its own DB for caching and what I want is to provide only a single instance for each record of the DB (so that when a property changes, all assigned variables will have that value and I can ensure that since no one on the main project will be able to create those classes and they will be provided to the variables by a manager class which will handle the single instantions).

Since I'm using SQLite-net for that and since it requires each instance to have an empty constructor, I need a way to only allow the SQLite and the PCL assemblies to create those subclasses declared on the main project(s) assembly(ies)

Update 2

I have no problem if the solution to this can be bypassed with Reflection because my main focus is to prevent people of doing new ClassImplA on the main project by simple mistake. However if possible I would like to have that so that stuff like JsonConvert.DeserializeObject<ClassImplA> would in fact fail with an exception.

Upvotes: 2

Views: 686

Answers (1)

Eugene Podskal
Eugene Podskal

Reputation: 10401

I may be wrong but none of the access modifiers will allow you to express such constraints - they restrict what other entities can see, but once they see it, they can use it.

  1. You may try to use StackTrace class inside the base class's constructor to check who is calling it:

    public class Base
    {
        public Base()
        {
            Console.WriteLine(
                new StackTrace()
                    .GetFrame(1)
                    .GetMethod()
                    .DeclaringType
                    .Assembly
                    .FullName);
        }
    }
    
    public class Derived : Base
    {
        public Derived() {        }
    }
    

With a bit of special cases handling it will probably work with Activator class , but isn't the best solution for obvious reasons (reflection, error-prone string/assembly handling).

  1. Or you may use some dependency that is required to do anything of substance, and that dependency can only be provided by your main assembly:

    public interface ICritical
    {
        // Required to do any real job
        IntPtr CriticalHandle { get; }
    }
    
    public class Base
    {
        public Base(ICritical critical) 
        { 
             if (!(critical is MyOnlyTrueImplementation)) 
                 throw ...  
        }
    }
    
    public class Derived : Base
    {
        // They can't have a constructor without ICritical and you can check that you are getting you own ICritical implementation.
        public Derived(ICritical critical) : base(critical) 
        {        }
    }
    

Well, other assemblies may provide their implementations of ICritical, but yours is the only one that will do any good.

  1. Don't try to prevent entity creation - make it impossible to use entities created in improper way.

Assuming that you can control all classes that produce and consume such entities, you can make sure that only properly created entities can be used.

It can be a primitive entity tracking mechanism, or even some dynamic proxy wrapping

public class Context : IDisposable
{
    private HashSet<Object> _entities;

    public TEntity Create<TEntity>()
    {
        var entity = ThirdPartyLib.Create(typeof(TEntity));
        _entities.Add(entity);
        return entity;
    }       

    public void Save<TEntity>(TEntity entity)
    {
        if (!_entities.Contains(entity))
            throw new InvalidOperationException();
        ...;
    }
}

It won't help to prevent all errors, but any attempt to persist "illegal" entities will blow up in the face, clearly indicating that one is doing something wrong.

  1. Just document it as a system particularity and leave it as it is.

One can't always create a non-leaky abstraction (actually one basically never can). And in this case it seems that solving this problem is either nontrivial, or bad for performance, or both at the same time.

So instead of brooding on those issues, we can just document that all entities should be created through the special classes. Directly instantiated objects are not guaranteed to work correctly with the rest of the system.

It may look bad, but take, for example, Entity Framework with its gotchas in Lazy-Loading, proxy objects, detached entities and so on. And that is a well-known mature library.

I don't argue that you shouldn't try something better, but that is still an option you can always resort to.

Upvotes: 2

Related Questions