2

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.

Cristiano Santos
  • 2,157
  • 2
  • 35
  • 53
  • With some limitations you can make constructor protected internal and make 3rd party assembly a friend. However...if they explicitly check for a public default ctor then this fails. If derived classes explicitly declare a public default ctor then it fails. If they override all your security checks with Reflection then it fails... – Adriano Repetti Jan 21 '17 at 16:28
  • @AdrianoRepetti please check my update regarding the **Reflection** case. And in relation to the `protected internal`, would the subclasses be able to use that? Because afaik, the `base()` would be unavailable for them since its not visible to the main project assemblies right? – Cristiano Santos Jan 21 '17 at 16:53
  • Protected will make it visible to subclasses and internal to friend assemblies. Honestly I'm not sure how Activator.CreateInstance will manage that. – Adriano Repetti Jan 21 '17 at 18:03
  • @AdrianoRepetti I just tried to change the constructor of `MyBaseClass` to `protected internal` but that still allowed me to instantiate the subclasses on their own assembly. Did I miss something? For the record, the subclasses will not be controlled by me and on my example test case, they have no constructor declared (which makes them use the default constructor) – Cristiano Santos Jan 21 '17 at 19:04

1 Answers1

2

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.

Community
  • 1
  • 1
Eugene Podskal
  • 10,270
  • 5
  • 31
  • 53
  • I searched about the `StackTrace` but everyone says its bad for performance. Anyway, I updated the description to reflect the reason about why I need this. And since SQLite always uses the empty constructor, I can't use the second approach right? – Cristiano Santos Jan 21 '17 at 16:08
  • 1
    @CristianoSantos `Bad for performance` depends on how often you will construct those objects. If it is some high-level dependency constructed once per application run, then it will usually be an acceptable price. Though if each and every DTO in the system will use reflection during its construction (like your new edit seems to suggest), then it may not be a good idea (on the other hand, one should always measure anything performance-related - it is possible that actual performance drop can be acceptable nonetheless). – Eugene Podskal Jan 21 '17 at 19:13