Motiv 1.0.7
See the version list below for details.
dotnet add package Motiv --version 1.0.7
NuGet\Install-Package Motiv -Version 1.0.7
<PackageReference Include="Motiv" Version="1.0.7" />
paket add Motiv --version 1.0.7
#r "nuget: Motiv, 1.0.7"
// Install Motiv as a Cake Addin #addin nuget:?package=Motiv&version=1.0.7 // Install Motiv as a Cake Tool #tool nuget:?package=Motiv&version=1.0.7
Motiv
Turn your if-statements into why did it do that statements
Motiv is a .NET library lets you decompose your logical expressions into logical propositions. By propositions we
mean a logical statement that can be either true
or false
, such as sun is shining or is even number. However,
this library goes a step further by allowing you to associate metadata with the proposition for when it is either
true
or false
and have it returned when resolved. The power that this library delivers is in the ability to
combine propositions to form new ones, and have the assertions (and optionally metadata) from the underlying
propositions filtered and aggregated so that the final result is a human-readable list of assertions (and optionally
metadata) that explain why the final result was either true
or false
.
What can I use the metadata for?
- User feedback - You require your application to provide detailed and accurate feedback to the user about why a certain decisions were made.
- Debugging - Quickly understand why a certain condition was met, or not. This can be especially useful in complex logical expressions where it might not be immediately clear which part of the expression was responsible for the final result.
- Multi-language Support - The metadata doesn't have to be a string. It can be any type, which means you could use it to support multi-lingual explanations.
- Rules Engine - The metadata can be used to conditionally select stateful objects, which can be used to implement a rules engine. This can be useful in scenarios where you need to apply different rules to different objects based on their state.
- Validation - The metadata can be used to provide human-readable explanations of why a certain validation rule was not met. This can be useful in scenarios where you need to provide feedback to the user about why a certain input was not valid.
- Parsing CLI arguments - The metadata can be used to conditionally map command-line arguments to collections of custom objects, which can then be used to drive different behaviors in your application.
Usage
The following example is a basic demonstration of how to use Motiv. It shows how to create a basic specification and then use it to determine if a number (3 in this case) is negative or not.
Basic specification
A basic specification can be created using the Spec
class. This class provides a fluent API for creating a
logical proposition
var isNegativeSpec =
Spec.Build((int n) => n < 0)
.Create("is negative");
var isNegative = isNegativeSpec.IsSatisfiedBy(-3);
isNegative.Satisfied; // true
isNegative.Reason; // "is negative"
isNegative.Assertions; // ["is negative"]
When negated, a proposition will return a reason prefixed with a !
character. This is useful for debugging
purposes.
var isNegative = isNegativeSpec.IsSatisfiedBy(3);
isNegative.Satisfied; // false
isNegative.Reason; // "!is negative"
isNegative.Assertions; // ["!is negative"]
you can also use the WhenTrue
and WhenFalse
methods to provide a more human-readable description for when the
outcome is either true
or false
. These values will be used in the Reason
and Assertions
properties of the result.
var isNegativeSpec =
Spec.Build((int n) => n < 0)
.WhenTrue("the number is negative")
.WhenFalse("the number is not negative")
.Create();
var isNegative = isNegativeSpec.IsSatisfiedBy(-3);
isNegative.Satisfied; // true
isNegative.Reason; // "the number is negative"
isNegative.Assertions; // ["the number is negative"]
If for whatever reason it is not appropriate to use the strings supplied to the WhenTrue
and WhenFalse
methods
to explain the outcome, you can instead provide a proposition, that will subsequently be used as a reason. This
can be useful when you want to provide a more detailed
var isNegativeSpec =
Spec.Build((int n) => n < 0)
.WhenTrue("the number is negative")
.WhenFalse("the number is not negative")
.Create("is negative");
var isNegative = isNegativeSpec.IsSatisfiedBy(-3);
isNegative.Satisfied; // true
isNegative.Reason; // "is negative"
isNegative.Assertions; // ["the number is negative"]
You are also not limited to strings. You can equally supply any POCO object and it will be yielded when appropriate.
var isNegativeSpec =
Spec.Build((int n) => n < 0)
.WhenTrue(new MyClass { Message = "the number is negative" })
.WhenFalse(new MyClass { Message = "the number is not negative" })
.Create("is negative")
var isNegative = isNegativeSpec.IsSatisfiedBy(-3);
isNegative.Satisfied; // true
isNegative.Reason; // "is negative"
isNegative.Assertions; // ["is negative"]
isNegative.Metadata; // [{ Message = "the number is negative" }]
Combining specifications
The real power of Motiv comes from combining specifications to form new ones. The library will take care of
collating the underlying causes and filter out irrelevant and inconsequential assertions and metadata from the final
result. Specifications can be combined using the &
,|
and ^
operators as well as the .ElseIf()
method.
var isNegativeSpec =
Spec.Build((int n) => n < 0)
.WhenTrue("the number is negative")
.WhenFalse(n => n == 0
? "the number is zero"
: "the number is positive")
.Create("is negative");
var isEvenSpec =
Spec.Build((int n) => n % 2 == 0)
.WhenTrue("the number is even")
.WhenFalse("the number is odd")
.Create("is even");
var isPositiveAndOddSpec = !isNegativeSpec & !isEvenSpec;
var isPositiveAndOdd = isPositiveAndOddSpec.IsSatisfiedBy(3);
isPositiveAndOdd.IsSatisfied; // returns true
isPositiveAndOdd.Reason; // "!is negative & !is even"
isPositiveAndOdd.Assertions; // ["the number is positive", "the number is odd"]
When you combine specifications to form new ones, only the specifications that helped determine the final result
will be included in the Assertions
property and Reason
property. Whereas the Assertions
property will simply
collect facts about evaluations, the Reason
property will preserve the expression by retaining the logical symbols
(e.g. &
, |
), but whilst also excluding any sub-expressions that played no part in determining the final result.
var isPositiveAndOdd = isPositiveAndOddSpec.IsSatisfiedBy(-3);
isPositiveAndOdd.IsSatisfied; // returns false
isPositiveAndOdd.Reason; // "is negative"
isPositiveAndOdd.Assertions; // ["the number is negative"]
Encapsulation and Re-use
You will likely want to encapsulate specifications for reuse across your application. For this typically have two
options, which is to either return specification instances from members of POCO objects, or to derive from the
Spec<TModel>
or Spec<TModel, TMetadata>
class (the former being merely syntactic sugar for Spec<TModel, string>
). Using these classes will help you to maintain a separation of concerns and raise the conspicuity of
important logic.
public class IsNegativeSpec : Spec<int>(
Spec.Build((int n) => n < 0)
.WhenTrue("the number is negative")
.WhenFalse("the number is not negative")
.Create());
public class IsNegativeMultiLingualSpec : Spec<int, MyClass>(
Spec.Build((int n) => n < 0)
.WhenTrue(new MyClass { Spanish = "el número es negativo" })
.WhenFalse(new MyClass { Spanish = "el número no es negativo" })
.Create("is negative"));
Problem Statement
This library deals with vexing issues from working with logic. Such as...
- Not knowing why your application did that After releasing an application and getting feedback from users it can be difficult trying figure out the specific reasons why an unexpected decision was arrived at, especially when there are numerous parameters involved. The more complex the overall logical expression, the more error-prone the solution is to supplement it with metadata/additional-functionality in order to answer this question.
- Unreadable blob of Logic When faced with the logical expression from hell it can be challenging to understand what bits of the logic played a pivotal role in producing the final result. Sure you can inspect the values but this is onerous, error-prone and slows you down.
- Blackbox Logic If you have gone down the laudable path of decomposing your logic into bite-sized chunks then you are faced with a new conundrum, which is comprehending what your logic is actually doing when revisiting it. Logic can be just as easily decomposed as easily as it can be composed, and this can lead to gotchas in your logic that are hard to stumble upon. This exacerbates the first problem Not knowing why your application did that.
Solution
Motiv addresses these challenges by extending the [Specification Pattern](https://en.wikipedia. org/wiki/Specification_pattern) so it can embed metadata along with logical statements. By following the same rules that govern traditional logical operators, the metadata is filtered and aggregated with metadata from adjacent logcal statements to form a list of metadata representing the underlying causes. You can think of it as a library that helps you supplement validation-like metadata to your regular/vanilla if-statements.
Benefits
- Decomposing Logic: In any non-trivial application there is a high chance that you will find a need to re-use logic in various places. This often means wrapping it in a function and moving it somewhere else. Motiv provides a framework for doing this and and the means to recombine them afterwards.
- Metadata association: Associate metadata for both
true
andfalse
outcomes. By default the metadata is a string - so that human-readable explanations of the logic can be defined alongside the actual logical expression. However, this doesn't have to be a string and can in fact be any type, which means that it can be used to support multi-lingual explanations, or even be used to conditionally select stateful objects. - Metadata accumulation: With complex logical expressions different underlying logic may (or may not) be
responsible producing the final result. This means that in order to be useful, the metadata needs to be selectively
filtered so that only the metadata from logic that contributed to the final result is accumulated, or to be more
technical: only the metadata from determinative operands are accumulated. For instance, with an or operation, if
one of the operands produces a
false
result and the other a true result then only the operand that returned atrue
result will have its metadata accumulated and the other operand's metadata will be ignored. - Enhanced Debugging Experience: This library has been designed to ease the developer experience around
important and/or complex Boolean logic. Specifications, whether composed of other Specifications or not,
override the
ToString()
method so that it provides a human-readable representation of its the logic tree. Furthermore, the generated result also accumulates a human-readable list of assertions why the result was eithertrue
orfalse
. This is primarily for debugging and troubleshooting purposes, but it could also be surfaced to users if so desired. - Simplified Testing: By extracting your logical expressions into separate classes you make it much easier to thoroughly test all the possible combinations that the parameters can be in. It also means the type from which the expressions were extracted now has potentially mock-able dependencies, which should make testing code-paths simpler.
Tradeoffs
- Performance: This library is designed to be as performant as possible, but it is still a layer of abstraction over the top of your logic. This means that there is a measurable performance cost to using it. However, this cost is negligible in most cases and are eclipsed by the benefits it provides.
- Dependency: This library is a dependency that you will have to manage,although it is as unobtrusive as possible and should not interfere with your other dependencies.
- Learning Curve: This library is a new concept and will nonetheless require some familiarization. That being said, it has been deliberately designed to be as intuitive and easy to use as possible - there is very little to learn, but a lot that can be expressed.
Getting Started with CLI
This section provides instructions on how to build and run the Motiv project using the .NET Core CLI, which is a powerful and flexible way to work with .NET projects.
Prerequisites
- Ensure you have the .NET SDK installed on your machine.
- Clone the repository to your local machine.
Building the Project
- Open Terminal or Command Prompt: Navigate to the directory where you cloned the Motiv repository.
- Navigate to the Project Directory: If the solution file (
.sln
) is not in the root, navigate to the directory containing the solution file. - Build the Solution: Run the following command to build the solution:
dotnet build
Running Tests
Run Unit Tests To execute tests within the solution run the following command:
dotnet test
Contribution
Your contributions to Motiv are greatly appreciated:
Branching Strategy:
Main Branches
main: This is the primary branch of the repository. It should always be stable and deployable. All development
branches are created from main, and features are merged back into it once they are complete and tested.
develop: This branch serves as an integration branch for features. Once a feature is complete, it is merged into
develop. When develop is stable and ready for a release, its contents are merged into main.
Supporting Branches
Feature Branches (feature/):
Created from: develop
Merged back into: develop
Naming convention: feature/ followed by a descriptive name (e.g., feature/add-login)
Purpose: Used for developing new features. Each feature should have its own branch.
Release Branches (release/):
Created from: develop
Merged back into: main and develop
Naming convention: release/ followed by the version number (e.g., release/v1.0.0)
Purpose: Used for preparing a new production release. Allows for last-minute dotting of i's and crossing of t's.
Workflow Summary
Start a new feature by creating a feature/ branch off develop.
Once the feature is complete, create a pull request to merge it back into develop.
Regularly merge develop into release branches for preparing releases.
Additional Notes
Delete branches post-merge to keep the repository clean.
Use pull requests for code review and ensure CI checks pass before merging.
Regularly update branches with the latest changes from their parent branch to avoid large merge conflicts.
This strategy helps in maintaining a clean and manageable workflow, ensuring stability in the main branch, and enabling continuous development and quick fixes as needed.
License
MIT License
Copyright (c) 2023 karlssberg
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
.NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
.NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
.NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
MonoAndroid | monoandroid was computed. |
MonoMac | monomac was computed. |
MonoTouch | monotouch was computed. |
Tizen | tizen40 was computed. tizen60 was computed. |
Xamarin.iOS | xamarinios was computed. |
Xamarin.Mac | xamarinmac was computed. |
Xamarin.TVOS | xamarintvos was computed. |
Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Microsoft.CSharp (>= 4.7.0)
-
net8.0
- Microsoft.CSharp (>= 4.7.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated | |
---|---|---|---|
7.4.3 | 85 | 8/1/2024 | |
7.4.2 | 61 | 7/31/2024 | |
7.4.1 | 79 | 7/24/2024 | |
7.4.0 | 100 | 7/19/2024 | |
7.3.0 | 107 | 6/26/2024 | |
7.2.0 | 99 | 6/25/2024 | |
7.1.0 | 99 | 6/24/2024 | |
7.0.4 | 88 | 6/24/2024 | |
7.0.3 | 103 | 6/23/2024 | |
7.0.2 | 136 | 6/17/2024 | |
7.0.1 | 98 | 6/12/2024 | |
7.0.0 | 146 | 5/4/2024 | |
6.0.0 | 128 | 4/29/2024 | |
5.0.0 | 120 | 4/28/2024 | |
4.0.0 | 130 | 4/27/2024 | |
3.1.1 | 114 | 4/26/2024 | |
3.1.0 | 132 | 4/26/2024 | |
3.0.0 | 129 | 4/20/2024 | |
2.0.0 | 146 | 4/1/2024 | |
1.0.7 | 145 | 3/29/2024 |