BenMorris / NetArchTest

A fluent API for .Net that can enforce architectural rules in unit tests.
MIT License
1.36k stars 81 forks source link

How to include members of base class? #135

Closed fingers10 closed 9 months ago

fingers10 commented 9 months ago

First thank you for this wonderful library. I'm using it more. I have added Encapsulation test to domain entities that all should have init, private, IReadonlyList or protected but not public.

Here is the test.

public class DomainTests
{
    [Fact]
    public void All_Entities_Should_Be_Encapsulated()
    {
        var result =
            Types.InAssembly(typeof(BaseEntity).Assembly)
                .That()
                .AreClasses()
                .And()
                .Inherit(typeof(BaseEntity))
                .Should()
                .MeetCustomRule(new EncapsulationRule())
                .GetResult();

        Assert.True(result.IsSuccessful);
    }
}

public class EncapsulationRule : ICustomRule
{
    public bool MeetsRule(TypeDefinition type)
    {
        return type.Properties
                   .All(x => x.SetMethod is null // For IReadOnlyList
                   || x.SetMethod.IsPrivate
                   || x.SetMethod.ReturnType.FullName.Contains("IsExternalInit")); // For C# 9 init
    }
}

The above code doesn't pull properties from Base Class.

For Example, consider the following

Base Class:

public abstract class BaseEntity
{
    protected BaseEntity()
    {
    }

    public Guid Id { get; init; }
    public Guid BranchId { get; protected set; }
    public bool Active { get; protected set; }
    public DateTimeOffset CreatedAt { get; protected set; }
    public string CreatedBy { get; protected set; }
    public DateTimeOffset? ModifiedAt { get; protected set; }
    public string ModifiedBy { get; protected set; }
}

public sealed class CustomerEntity : BaseEntity
{
    private readonly List<CustomerDueEntity> _customerDues = new();

    public CustomerName Name { get; private set; }
    public CustomerMobile MobileNumber { get; private set; }
    public CustomerEmail Email { get; private set; }
    public CustomerAddress Address { get; private set; }
    public CustomerGsTin GsTin { get; private set; }
    public Money DueAmount { get; private set; }
    public CustomerLoyaltyPoint LoyaltyPointEarned { get; private set; } = CustomerLoyaltyPoint.None;
    public CustomerLoyaltyPoint LoyaltyPointReedemed { get; private set; } = CustomerLoyaltyPoint.None;
    public CustomerLoyaltyPoint LoyaltyPoint => LoyaltyPointEarned - LoyaltyPointReedemed;
    public CustomerStatus Status { get; private set; }
    public DiscountPercentage DiscountPercentage { get; private set; }

    public IReadOnlyList<CustomerDueEntity> CustomerDues => _customerDues.ToList();
}

I'm not able to find any properties from BaseEntity in the TypeDefinition of my CustomRule.

Please can you assist me on what I'm missing?

NeVeSpl commented 9 months ago

TypeDefinition only contains members that are defined in the given type, to get members from a base type, you need to follow the inheritance hierarchy: You will find a TypeReference to the base class here: TypeDefinition.BaseType to convert it to TypeDefinition: TypeDefinition.BaseType?.Resolve()

fingers10 commented 9 months ago

@NeVeSpl Thanks for the inputs. I have updated my rule as shown below.

public class EncapsulationRule : ICustomRule
{
    public bool MeetsRule(TypeDefinition type)
    {
        return TypeShouldNotHavePublicSetters(type) && TypeShouldNotHavePublicSetters(type.BaseType?.Resolve());
    }

    private bool TypeShouldNotHavePublicSetters(TypeDefinition? type)
    {
        return type?.Properties
                   .All(x => x.SetMethod is null // For IReadOnlyList
                   || !x.SetMethod.IsPublic // Allow Private and Protected
                   || x.SetMethod.ReturnType.FullName.Contains("IsExternalInit")) // For C# 9 init
                   ?? true;
    }
}