Type rules

edit

A type rule selects types, then asserts something about them: the types they use with Assert.Architecture, or their names and shape with Assert.All.

Select the types

ArchitectureModel.Types is a list of TypeElement: select the types with LINQ. The failure messages show the expression you pass to the assertion, so a variable with a clear name, such as domain, gives a clear message.

These members of TypeElement cover the common filters:

MemberIs true when the type
IsInNamespaceis in the namespace, not in one of its child namespaces
IsInNamespaceOrChildrenis in the namespace or one of its child namespaces: MyApp.Domain matches MyApp.Domain.Events, not MyApp.DomainEvents
IsClass, IsInterface, IsStruct, IsEnum, IsDelegate, IsRecordis of a kind
IsAbstract, IsSealed, IsStatic, IsPublic, IsNestedhas a modifier
InheritsFrom, Implements, IsAssignableToinherits from a class or implements an interface, directly or not
HasAttributehas an attribute

Name is the name without the namespace and the generic arity (Repository for Repository<T>), so t.Name.EndsWith("Repository", StringComparison.Ordinal) filters by name.

The members that take a type, such as Implements<T>(), compare full names. Pass a generic type definition, such as typeof(Repository<>), to match all its instantiations. The overloads that take a string expect the name as written in metadata: MyApp.Repository`1, MyApp.Outer+Inner.

Combine sets with LINQ (Except, Concat) or with a collection expression: [.. application, .. web].

Check the dependencies

AssertionFails when a type
DoesNotDependOn(types, forbiddenTypes)uses one of the forbidden types. The dependencies between the checked types are ignored
DoesNotDependOn(types, predicate)uses a type that matches the predicate, including a type of .NET or of a package
DoesNotDependOn<T>(types)uses T. A generic type definition, such as typeof(List<>), forbids all its instantiations
DependsOnlyOn(types, allowedTypes)uses a type of the loaded assemblies that is neither allowed nor checked. The types of .NET and of the packages are not checked
DependsOnlyOn(types, predicate)uses a type that is not checked and does not match the predicate. Add System to allow the types of .NET
DoesNotAccessMember<T>(types, memberName)calls a method or a constructor of T, or reads or writes a field, a property or an event

The predicates take a TypeReference, which has the same IsInNamespace and IsInNamespaceOrChildren members as TypeElement:

var architecture = ArchitectureModel.FromAssemblyContaining<Order>();
var domain = architecture.Types.Where(t => t.IsInNamespaceOrChildren("SampleApp.Domain"));

// The types of the domain may use each other and the types of .NET, nothing else
Assert.Architecture.DependsOnlyOn(domain, t => t.IsInNamespaceOrChildren("System"));

DoesNotAccessMember bans a member of a type that is otherwise allowed, such as the current time:

var architecture = ArchitectureModel.FromAssemblyContaining<Order>();
var types = architecture.Types.Where(t => t.IsInNamespaceOrChildren("SampleApp"));

Assert.Architecture.DoesNotAccessMember<DateTime>(types, "Now", "the current time comes from TimeProvider");
Assert.Architecture.DoesNotAccessMember<DateTime>(types, nameof(DateTime.UtcNow), "the current time comes from TimeProvider");

The model explains what counts as a dependency.

Check the names and the shape

Assert.All from Meziantou.Framework.Assertions checks a predicate on each type, with the members of TypeElement:

var architecture = ArchitectureModel.FromAssemblyContaining<Order>();
var repositories = architecture.Types.Where(t => t.IsClass && t.Implements<IOrderRepository>());

Assert.NotEmpty(repositories);
Assert.All(repositories, t => t.Name.EndsWith("Repository", StringComparison.Ordinal));
Assert.All(repositories, t => t.IsSealed);
Assert.All(architecture.Types.Where(t => t.IsInNamespaceOrChildren("SampleApp.Domain") && !t.IsInterface), t => t.IsImmutable);
  • Assert.All passes when the collection is empty: add Assert.NotEmpty when a typo in the filter must fail.
  • Assert.All reports the first type that does not match the predicate.
  • IsImmutable is true when the instance fields are readonly or constant, and the instance properties have no setter other than init.

The members of the types are in the model too, so the same pattern checks methods, properties or fields:

var architecture = ArchitectureModel.FromAssemblyContaining<Order>();
var handlers = architecture.Types.Where(t => t.IsInNamespace("SampleApp.Application") && t.Name.EndsWith("Handler", StringComparison.Ordinal));

Assert.All(handlers.SelectMany(t => t.Methods).Where(method => method.Accessibility == ElementAccessibility.Public), method => method.IsAsync);