TNG / ArchUnitNET

A C# architecture test library to specify and assert architecture rules in C# for automated testing.
Apache License 2.0
826 stars 55 forks source link

How to check for system members? #198

Open simonthum opened 1 year ago

simonthum commented 1 year ago

I have difficulties checking for unwanted system references - I failed to jot them down in a statically typed way.

Consider:

IArchRule rule1 =
    Classes().Should().NotCallAny("System.DateTime::get_Now");

This works but is not exactly intuitive. I went on to try:

IArchRule rule2 =
    Classes().Should().NotCallAny(
    MethodMembers().That().AreDeclaredIn(
        Types(true).That().HaveFullName(typeof(DateTime).FullName)));

But there is no MethodMembers(true) that would allow to check for members of system types this way.

Is there another way I'm missing to achieve rule1 statically typed?

x789 commented 1 year ago

I would also like to implement a test that only certain types can access the system time. However, the solution mentioned by @simonthum unfortunately does not work for me.

I tried unsuccessfully with Classes().Should().NotCallAny(MethodMembers().That().AreDeclaredIn(typeof(System.DateTimeOffset)));

However, this is reasonable to me since _ = DateTimeOffset.Now is an access to a property. However, I cannot replace MethodMembers with PropertyMembers because the result is not accepted by NotCallAny.

I wonder if there is a way to check for property access, or is this functionality not (yet) part of ArchUnitNET?

x789 commented 1 year ago

I was wrong -- Properties are a C# feature which are transformed into methods by the compiler. After specifying an empty parameter list and adding the assembly of DateTimeOffset to the architecture, it worked:

var rule = Types().That().ResideInAssembly(myAssembly).Should().NotCallAny("System.DateTimeOffset::get_Now()");

I didn't find a way to define the rule strongly typed either. For the meantime I'm using an extension method. Maybe this is an acceptable workaround for others.

// Extension method
public static IArchRule NotAccessGetter<TType>(this ObjectsShould<TypesShouldConjunction, IType> builder, object? property, [CallerArgumentExpression("property")] string typeAndPropertyName = null!)
{
  var names = typeAndPropertyName.Split('.');
  if (names[0] != typeof(TType).Name) throw new ArgumentException($"'{names[1]}' is not part of '{names[0]}'.");

  return builder.NotCallAny(MethodMembers().That().AreDeclaredIn(typeof(TType)).And().HaveName($"get_{names[1]}()"));
}
// Usage example
[Fact]
public void Test1()
{
  // Remember to add the assembly that contains DateTimeOffset to your `architecture`.
  var types = Types().That().ResideInAssembly(myAssembly);
  types.Should().NotAccessGetter<DateTimeOffset>(DateTimeOffset.Now).Check(architecture);
  types.Should().NotAccessGetter<DateTimeOffset>(DateTimeOffset.UtcNow).Check(architecture);
}
simonthum commented 1 year ago

@x789 I like that creative solution, although having to actually call the Property may make it difficult to use in some contexts.