FriendClass allows to limit access to 'internal' members by specifying a fixed list of friend types.
Packages that are part of the FriendClass module:
FriendClass provides 2 types:
To start using FriendClass, reference either the Durian.FriendClass or Durian package.
Note: Like with other Durian modules, the target project must reference the Durian.Core package as well.
To limit access of 'internal' members, specify a Durian.FriendClassAttribute with the target friend type as argument.
using Durian;
[FriendClass(typeof(A))]
public class Test
{
internal static string Key { get; }
}
public class A
{
public string GetKey()
{
// Success!
// Type 'A' is a friend of 'Test', so it can safely access internal members.
return Test.Key;
}
}
public class B
{
public string GetKey()
{
// Error!
// Type 'B' is not a friend of 'Test', so it cannot access internal members.
return Test.Key;
}
}
A single type can have multiple friend types.
using Durian;
[FriendClass(typeof(A))]
[FriendClass(typeof(B))]
public class Test
{
internal static string Key { get; }
}
public class A
{
public string GetKey()
{
// Success!
// Type 'A' is a friend of 'Test', so it can safely access internal members.
return Test.Key;
}
}
public class B
{
public string GetKey()
{
// Success!
// Type 'B' is a friend of 'Test', so it can safely access internal members.
return Test.Key;
}
}
To ensure proper usage, FriendClass enforces a set of limitations on a potential target and its friend types.
Durian.FriendClassAttribute can be applied to classes, structs and records. Other type members, like enums, interfaces and delegates are not allowed.
FriendClass protects all 'internal' members of the target type, including instance and static members, constructors, inner types etc.
using Durian;
[FriendClass(typeof(A))]
public class Test
{
internal readonly string _key;
internal Test(string key)
{
_key = key;
}
internal static Test Default { get; } = new Test("default");
internal class Inner
{
}
}
public class A
{
public string GetKey()
{
// Success!
// Type 'A' is a friend of 'Test', so it can safely access all available kinds of internal members.
Test custom = new Test("custom");
Test def = Test.Default;
Test.Inner inner = new Test.Inner();
return def._key;
}
}
public class B
{
public string GetKey()
{
// Error!
// Type 'B' is not a friend of 'Test', so it has no access to any of the internal members.
Test custom = new Test("custom");
Test def = Test.Default;
Test.Inner inner = new Test.Inner();
return def._key;
}
}
Members with the 'protected internal' are also protected, but only against external access - inherited classes still can access them.
using Durian;
[FriendClass(typeof(A))]
public class Test
{
protected internal static string Key { get; }
}
public class A
{
public string GetKey()
{
// Success!
// Class 'A' is a friend of 'Test'.
return Test.Key;
}
}
public class B
{
public string GetKey()
{
// Error!
// Class 'B' is NOT a friend of 'Test'.
return Test.Key;
}
}
public class C : Test
{
public string GetKey()
{
// Success!
// Class 'A' is a child of 'Test', so Key is not protected.
return Test.Key;
}
}
Inner types of the target type are implicit friends, meaning they don't have to be specified using the Durian.FriendClassAttribute. This behavior cannot be changed.
using Durian;
// FriendClassAttribute is redundant - Inner1 is an implicit friend type.
[FriendClass(typeof(Inner1))]
public class Test
{
internal static string Key { get; }
class Inner1
{
public string GetKey()
{
// Success!
// 'Key' can be accessed, because 'Inner1' is an implicit friend type.
return Key;
}
}
class Inner2
{
public string GetKey()
{
// Success!
// 'Key' can be accessed, because 'Inner2' is an implicit friend type.
return Key;
}
}
}
The same rule applies to inner types of friends.
using Durian;
[FriendClass(typeof(Other))]
public class Test
{
internal static string Key { get; }
}
public class Other
{
public string GetKey()
{
// Success!
// Type 'Other' is a friend of 'Test', so it can safely access internal members.
return Test.Key;
}
public class Inner
{
public string GetKey()
{
// Success!
// Type 'Inner' is an inner type of friend of 'Test', so it can safely access internal members.
return Test.Key;
}
}
}
Inner types can also be friend types.
using Durian;
[FriendClass(typeof(Other.Inner))]
public class Test
{
internal static string Key { get; }
}
public class Other
{
public string GetKey()
{
// Error!
// Type 'Other' is not a friend of 'Test', so it cannot access internal members.
return Test.Key;
}
public class Inner
{
public string GetKey()
{
// Success!
// Type 'Inner' is an inner type of friend of 'Test', so it can safely access internal members.
return Test.Key;
}
}
}
FriendClass fully supports class inheritance and provides multiple ways to configure it.
By default, child types of target are not considered friends.
using Durian;
[FriendClass(typeof(Other))]
public class Test
{
internal static string Key { get; }
}
public class Other
{
}
public class Child : Test
{
public string GetKey()
{
// Error!
// Type 'Child' is not a friend of 'Test', so it cannot access internal members.
return Key;
}
}
The same rules applies to children of friends.
using Durian;
[FriendClass(typeof(Other))]
public class Test
{
internal static string Key { get; }
}
public class Other
{
}
public class Child : Other
{
public string GetKey()
{
// Error!
// Type 'Child' is not a friend of 'Test', so it cannot access internal members.
return Test.Key;
}
}
However, both features can be easily configured - children of target using the AllowChildren property of the Durian.Configuration.FriendClassConfigurationAttribute...
using Durian;
using Durian.Configuration;
[FriendClass(typeof(Other))]
[FriendClassConfiguration(AllowChildren = true)]
public class Test
{
internal static string Key { get; }
}
public class Other
{
}
public class Child : Test
{
public string GetKey()
{
// Success!
// Type 'Child' is an inner type of friend of 'Test', so it can safely access internal members.
return Key;
}
}
...and children of friends using the AllowFriendChildren property of the Durian.FriendClassAttribute.
using Durian;
[FriendClass(typeof(Other), AllowFriendChildren = true)]
public class Test
{
internal static string Key { get; }
}
public class Other
{
}
public class Child : Other
{
public string GetKey()
{
// Success!
// Type 'Child' is an inner type of friend of 'Test', so it can safely access internal members.
return Test.Key;
}
}
Note: Setting the AllowChildren property of the Durian.Configuration.FriendClassConfigurationAttribute to true on structs or sealed/static classes is not allowed.
By default, inherited 'internal' members are not protected against external usage.
using Durian;
[FriendClass(typeof(Other))]
public class Test : Parent
{
}
public class Parent
{
internal string Name { get; set; }
}
public class Other
{
}
public class NotFriend
{
public string GetName()
{
// Success! Inherited 'internal' members are not protected.
Test test = new();
test.Name = "";
return test.Name;
}
}
If such behaviour is not desirable, it can be easily configured.
using Durian;
using Durian.Configuration;
[FriendClass(typeof(Other))]
[FriendClassConfiguration(IncludeInherited = true)]
public class Test : Parent
{
}
public class Parent
{
internal string Name { get; set; }
}
public class Other
{
}
public class NotFriend
{
public string GetName()
{
// Error! Inherited member cannot be accessed.
Test test = new();
test.Name = "";
return test.Name;
}
}
Note: Setting the IncludeInherited property of the Durian.Configuration.FriendClassConfigurationAttribute to true on structs or static classes or classes with direct base type other than System.Object is not allowed.
Even with the IncludeInherited property set to true, inherited static members still can be accessed, but with a proper diagnostic.
using Durian;
using Durian.Configuration;
[FriendClass(typeof(Other))]
[FriendClassConfiguration(IncludeInherited = true)]
public class Test : Parent
{
}
public class Parent
{
internal static string Name { get; set; }
}
public class Other
{
}
public class NotFriend
{
public void SetName()
{
// Success! Inherited static members are not protected.
Test.Name = "";
}
}
Members with the 'internal' modifier specified in other assembly can be accessed if the System.Runtime.CompilerServices.InternalsVisibleToAttribute is applied.
using Durian;
using Durian.Configuration;
[FriendClass(typeof(Other))]
[FriendClassConfiguration(IncludeInherited = true)]
public class Test : Parent
{
}
public class Other
{
}
public class NotFriend
{
public string GetName()
{
// Error! Inherited member cannot be accessed.
Test test = new();
test.Name = "";
return test.Name;
}
}
// In different assembly...
public class Parent
{
internal string Name { get; set; }
}
(Written by Piotr Stenke)