Motiv
flowchart BT
A --> P((" Why Motiv? "))
B(["composable"]) --> A((AND))
C((AND)) --> A
D(["reusable"]) --> C
E(["explainable"]) --> C
style E stroke:darkcyan,stroke-width:2px
style D stroke:darkcyan,stroke-width:2px
style B stroke:darkcyan,stroke-width:2px
style P stroke:darkcyan,stroke-width:4px
Know Why, not just What
Motiv is a developer-first .NET library that transforms the way you work with boolean logic. It lets you form expressions from discrete propositions so that you can explain why decisions were made.
To demonstrate Motiv in action, let's create some atomic propositions:
// Define propositions
var isValid = Spec.Build((int n) => n is >= 0 and <= 11).Create("valid");
var isEmpty = Spec.Build((int n) => n == 0).Create("empty");
var isFull = Spec.Build((int n) => n == 11).Create("full");
Then compose using boolean operators:
// Compose a new proposition
var isPartiallyFull = isValid & !(isEmpty | isFull);
And evaluate to get detailed feedback:
// Evaluate the proposition
var result = isPartiallyFull.IsSatisfiedBy(5);
result.Satisfied; // true
result.Assertions; // ["valid", "¬empty", "¬full"]
result.Reason; // "valid & !(¬empty | ¬full)"
Assertions will bubble up the root of the proposition tree (if they contributed to the final results). This can be seen in the following flowchart:
%%{init: { 'themeVariables': { 'fontSize': '13px' }}}%%
flowchart BT
AND --> P((" Is partially \n full?"))
B([""valid""]) --> AND((AND))
NOT((NOT)) --> AND
OR((OR)) --> NOT
D([""¬empty""]) --> OR
E([""¬full""]) --> OR
style E stroke:darkcyan,stroke-width:2px
style D stroke:darkcyan,stroke-width:2px
style B stroke:darkcyan,stroke-width:2px
style P stroke:darkcyan,stroke-width:4px
Installation
Motiv is available as a NuGet Package. Install it using one of the following methods:
NuGet Package Manager Console:
Install-Package Motiv
.NET CLI:
dotnet add package Motiv
Basic Usage
Let's start with an example of a minimal/atomic proposition to demonstrate Motiv's core concepts. Take the example of determining if a number is even:
%%{init: { 'themeVariables': { 'fontSize': '13px' }}}%%
flowchart BT
True([""is even""]) -->|true| P(("is even?"))
False([""¬is even""]) -->|false| P
style P stroke:darkcyan,stroke-width:4px
style True stroke:darkgreen,stroke-width:2px
style False stroke:darkred,stroke-width:2px
// Define a atomic proposition
var isEven = Spec.Build((int n) => n % 2 == 0).Create("is even");
// Evaluate the proposition
var result = isEven.IsSatisfiedBy(2);
result.Satisfied; // true
result.Reason; // "is even"
result.Assertions; // ["is even"]
This minimal example showcases how easily you can create and evaluate propositions with Motiv.
Advanced Usage
Explicit Assertions
For more descriptive results, use WhenTrue()
and WhenFalse()
to define custom assertions.
Continuing with the previous example, let's provide more explicit feedback when the number is odd:
%%{init: { 'themeVariables': { 'fontSize': '13px' }}}%%
flowchart BT
True([""is even""]) -->|true| P(("is even?"))
False([""is odd""]) -->|false| P
style P stroke:darkcyan,stroke-width:4px
style True stroke:darkgreen,stroke-width:2px
style False stroke:darkred,stroke-width:2px
var isEven =
Spec.Build((int n) => n % 2 == 0)
.WhenTrue("is even")
.WhenFalse("is odd")
.Create();
var result = isEven.IsSatisfiedBy(3);
result.Satisfied; // false
result.Reason; // "is odd"
result.Assertions; // ["is odd"]
Custom Metadata
For scenarios requiring more context, you can use metadata instead of simple string assertions. For example, let's instead attach metadata to our example:
%%{init: { 'themeVariables': { 'fontSize': '13px' }}}%%
flowchart BT
True(["new MyMetadata("even")"]) -->|true| P(("is even?"))
False(["new MyMetadata("odd")"]) -->|false| P
style P stroke:darkcyan,stroke-width:4px
style True stroke:darkgreen,stroke-width:2px
style False stroke:darkred,stroke-width:2px
var isEven =
Spec.Build((int n) => n % 2 == 0)
.WhenTrue(new MyMetadata("even"))
.WhenFalse(new MyMetadata("odd"))
.Create("is even");
var result = isEven.IsSatisfiedBy(2);
result.Satisfied; // true
result.Reason; // "is even"
result.Assertions; // ["is even"]
result.Value; // { Text: "even" }
Composing Propositions
Motiv's true power shines when composing logic from simpler propositions, and then using their results to create new assertions. To demonstrate this, we are going to solve the classic Fizz Buzz problem using Motiv. In this problem, we need to determine if a number is divisible by 3, 5, or both, and then provide the appropriate feedback for each case.
Below is the flowchart of our solution:
%%{init: { 'themeVariables': { 'fontSize': '13px' }}}%%
flowchart BT
TrueOr((OR)) -->|true| P(("is substitution?"))
False(["n"]) -->|false| P
TrueIsFizz(("fizz?")) -->|true| TrueOr
TrueIsBuzz(("buzz?")) -->|true| TrueOr
TrueIsFizzTrue([""fizz""]) -->|true| TrueIsFizz
TrueIsBuzzTrue([""buzz""]) -->|true| TrueIsBuzz
style P stroke:darkcyan,stroke-width:4px
style TrueOr stroke:darkgreen,stroke-width:2px
style TrueIsFizz stroke:darkgreen,stroke-width:4px
style TrueIsBuzz stroke:darkgreen,stroke-width:4px
style TrueIsFizzTrue stroke:darkgreen,stroke-width:2px
style TrueIsBuzzTrue stroke:darkgreen,stroke-width:2px
style False stroke:darkred,stroke-width:2px
This is then implemented in code as follows:
// Define atomic propositions
var isFizz = Spec.Build((int n) => n % 3 == 0).Create("fizz");
var isBuzz = Spec.Build((int n) => n % 5 == 0).Create("buzz");
// Compose atomic propositions and redefine assertions
var isSubstitution =
Spec.Build(isFizz | isBuzz)
.WhenTrue((_, result) => string.Concat(result.Assertions)) // Concatenate "fizz" and/or "buzz"
.WhenFalse(n => n.ToString())
.Create("is substitution");
isSubstitution.IsSatisfiedBy(15).Value; // "fizzbuzz"
isSubstitution.IsSatisfiedBy(3).Value; // "fizz"
isSubstitution.IsSatisfiedBy(5).Value; // "buzz"
isSubstitution.IsSatisfiedBy(2).Value; // "2"
This example demonstrates how you can compose complex propositions from simpler ones using Motiv.
Custom Types and Reuse
Motiv provides some classes to inherit from so that you can create your own strongly typed propositions which can be reused across your codebase.
For example, let's create a strongly typed proposition to determine if a number is even:
public class IsEven() : Spec<int>(
Spec.Build((int n) => n % 2 == 0)
.WhenTrue("is even")
.WhenFalse("is odd")
.Create();
This can then be instantiated where needed and used as-is. Also, by making it strongly typed, you can ensure that there is no ambiguity when registering it with a DI container.
When to Use Motiv
Motiv is not meant to replace all your boolean logic. You should only use it when it makes sense to do so. If your logic is pretty straightforward or does not really need any feedback about the decisions being made, then you might not see a big benefit from using Motiv. It is just another tool in your toolbox, and sometimes the simplest solution is the best fit.
Consider using Motiv when you need two or more of the following:
- Visibility: Granular, real-time feedback about decisions
- Decomposition: Break down complex logic into meaningful subclauses
- Reusability: Avoid logic duplication across your codebase
- Modeling: Explicitly model your domain logic (e.g., for Domain-Driven Design)
- Testing: Test your logic in isolation—without mocking dependencies
Tradeoffs
- Performance: Motiv is not designed for high-performance scenarios where every nanosecond counts. Its focus is on maintainability and readability, although in most use-cases the performance overhead is negligible.
- Dependency: Once embedded in your codebase, removing Motiv can be challenging. However, it does not depend on any third-party libraries itself, so it won't bring any unexpected baggage.
- Learning Curve: New users may need time to adapt to Motiv's approach and API