What you describe is partially possible. You can eliminate the need for them to explicitly reference the hidden assembly, but that assembly will still get pulled in at compiled time, and required at runtime.
Let's say you have these classes defined:
// in assembly 1:
public class A
{
public virtual void Foo() { }
}
// and in assembly 2:
// requires explicit reference to assembly 1 to use
public class B : A
{
public override void Foo() { }
public A Value { get; set; }
public void Foo(A value) { }
}
// has implicit reference to assembly 1, but end user can ignore
public class C
{
private A Value { get; set; }
internal void Foo(A value) { }
protected internal A Bar() { return new A(); }
}
// usable at runtime even if assembly 1 is missing, as long as you don't call Foo()
public class D
{
public void Foo() { A blah = new A(); }
public void Bar() { }
}
If the end user uses class B, they will require an explicit reference to assembly 1. Since A is part of B's public interface, in order to use B, you have to know about A. There are 3 different public references to A, and any of them will require knowing about A to use B.
However, class C makes references to A, but all references are private/internal/local. Since every reference to A is hidden from the outside, the end user doesn't have to explicitly know about assembly 1. It will still be required at runtime, but you don't have to add it as a reference, it's an indirect reference.
And if the end user uses class D, without ever using B or C, assembly 1 will only get loaded if you call D.Foo(), which has a local variable of type A. You can actually use D.Bar() freely even if assembly 1 is completely missing at runtime. Although if you call D.Foo() and assembly 1 is missing, you'll get an exception.