Question

I just started evaluating PostSharp Ultimate and i want to enforce some custom architecture constraints inside of an assembly.

The assembly is structured like this: (basically for each namespace there is a root interface and specific implementations of that interface)

MycompanyNamespace.Core.CommandDispatcher
  ICommandDispatcher
    XCommandDispatcher
    YCommandDispatcher
    ...

MycompanyNamespace.Core.Services
  IService
    XService
    YService
    ...

MycompanyNamespace.Core.Provider
  IProvider
    XProvider
    YProvider
    ...

The rule i want to enforce:

  • Upstream references are not allowed e.g. a type in the namespace in which the IProvider interface is declared is not allowed to reference a type in the namespace in which the IService or ICommandDispatcher type is declared
  • Downstream references are allowed

I already tried the ComponentInteral constraint that is shipped with PostSharp and also created a custom ReferentialConstraint.

I am not sure

  • whats better, to use a positive or a negative rule? e.g.

    [GrantAccess(TargetNamespace = typeof(IProvider), GrantedBy = typeof(ICommandDispatcher), typeof(IService)]

    [ProhibitAccess(TargetNamespace = typeof(ICommandDispatcher), ProhibitedBy = typeof(IProvider), typeof(IService)]

  • can I put the rules into the GlobalApsects.cs file for the specific assemlby or do I need to decorate the types with the attribute?

  • can I reuse a preshipped rule? or how would someone implement such a custom rule?

Was it helpful?

Solution

GrantAccess or ProhibitAccess

I wouldn't say that only one options is definitively better than the other when choosing between GrantAccess and ProhibitAccess approaches. It depends on the details of your design and how strictly you want to enforce the constraints.

Questions that come to mind are:

Will the classes in the given namespace be public or internal to this assembly?

What if a new namespace/component is added in the future? Should the classes from the new namespace be able to access this namespace by default? Or should this access be granted explicitly?

Let's say that all the classes in the example are internal and you want to enforce reference constraints between them. GrantAccess gives you more strict control in this case. Newly added namespace will not be able to access the existing namespaces unless you review the design and grant this access explicitly.

Or you may want to make your Services classes public and just restrict their use from the Provider namespace. Having to explicitly add GrantAccess for each external namespaces that uses Services is quite inconvenient. The less restrictive ProhibitAccess approach may be better in this case.

These are just the examples of judgement you can make, but in the end it depends on your design and your project.

Assembly or type level attribute

Since you want to apply the constraint to all the classes in the given namespace, it's more convenient to apply the attribute to the assembly (in GlobalAspects.cs) and specify the namespace in the AttributeTargetTypes property.

// Grant access to all classes in MycompanyNamespace.Core.Services namespace
[assembly: GrantAccess(..., AttributeTargetTypes = "MycompanyNamespace.Core.Services.*", ...)]

Custom constraint

The ComponentInternal attribute provided by PostSharp implements the GrantAccess approach, so you can apply it for that use case. However, there seems to be a bug that doesn't allow it to be applied on assembly level more than once. This should be fixed in one of the upcoming releases.

To implement your custom constraint with a similar logic, you can start with the following example:

[MulticastAttributeUsage(
    MulticastTargets.AnyType | MulticastTargets.Method | MulticastTargets.InstanceConstructor | MulticastTargets.Field,
    TargetTypeAttributes = MulticastAttributes.UserGenerated,
    TargetMemberAttributes = (MulticastAttributes.AnyVisibility & ~MulticastAttributes.Private) | MulticastAttributes.UserGenerated)]
[AttributeUsage(
    AttributeTargets.Assembly |
    AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface | AttributeTargets.Enum | AttributeTargets.Delegate,
    AllowMultiple = true)]
public class GrantAccessAttribute : ReferentialConstraint
{
    private string[] namespaces;

    public GrantAccessAttribute(params Type[] types)
    {
        this.namespaces = types.Select(t => t.Namespace).ToArray();
    }

    public override void ValidateCode(object target, Assembly assembly)
    {
        MemberInfo targetMember = target as MemberInfo;

        if (targetMember != null)
        {
            Type targetType = target as Type;

            if (targetType != null)
            {
                // validate derived types
                foreach (TypeInheritanceCodeReference reference in ReflectionSearch.GetDerivedTypes(targetType, ReflectionSearchOptions.IncludeTypeElement))
                {
                    Validate(reference);
                }

                // validate member references
                foreach (MemberTypeCodeReference reference in ReflectionSearch.GetMembersOfType(targetType, ReflectionSearchOptions.IncludeTypeElement))
                {
                    Validate(reference);
                }
            }

            // validate references in methods
            foreach (MethodUsageCodeReference methodUsageCodeReference in ReflectionSearch.GetMethodsUsingDeclaration(targetMember))
            {
                Validate(methodUsageCodeReference);
            }
        }
    }

    private void Validate(ICodeReference codeReference)
    {
        string usingNamespace = GetNamespace(codeReference.ReferencingDeclaration);
        string usedNamespace = GetNamespace(codeReference.ReferencedDeclaration);

        if (usingNamespace.Equals(usedNamespace, StringComparison.Ordinal))
            return;

        if (this.namespaces.Any(
            x => usingNamespace.Equals(x, StringComparison.Ordinal) ||
                 (usingNamespace.StartsWith(x, StringComparison.Ordinal) && usingNamespace[x.Length] == '.')))
            return;

        object[] arguments = new object[] {/*...*/};
        Message.Write(MessageLocation.Of(codeReference.ReferencingDeclaration), SeverityType.Warning, "ErrorCode", "Access error message.", arguments);
    }

    private string GetNamespace(object declarationObj)
    {
        Type declaringType = declarationObj as Type;

        if (declaringType == null)
        {
            MemberInfo declaringMember;
            ParameterInfo parameter;
            LocationInfo location;

            if ((declaringMember = declarationObj as MemberInfo) != null)
            {
                declaringType = declaringMember.DeclaringType;
            }
            else if ((location = declarationObj as LocationInfo) != null)
            {
                declaringType = location.DeclaringType;
            }
            else if ((parameter = declarationObj as ParameterInfo) != null)
            {
                declaringType = parameter.Member.DeclaringType;
            }
            else
            {
                throw new Exception("Invalid declaration object.");
            }
        }

        return declaringType.Namespace;
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top