Skip to content

Commit

Permalink
DOCUMENTATION: Foundations Enhancement
Browse files Browse the repository at this point in the history
Closes: #284
  • Loading branch information
glhays committed Jul 22, 2024
1 parent f6b79ce commit e8e368a
Showing 1 changed file with 39 additions and 33 deletions.
72 changes: 39 additions & 33 deletions 2. Services/2.1 Foundations/2.1 Foundations.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ For instance, if a storage broker is offering `InsertStudentAsync(Student studen
```csharp
public async ValueTask<Student> AddStudentAsync(Student student)
{
ValidateStudent(student);
ValidateStudentOnAdd(student);

return await this.storageBroker.InsertStudentAsync(student);
}
Expand Down Expand Up @@ -53,13 +53,13 @@ But they offer a validation and exception handling (and mapping) wrapper around
public ValueTask<Student> AddStudentAsync(Student student) =>
TryCatch(async () =>
{
ValidateStudent(student);
ValidateStudentOnAdd(student);

return await this.storageBroker.InsertStudentAsync(student);
});
```

In the above method, you can see `ValidateStudent` function call preceded by a `TryCatch` block.
In the above method, you can see `ValidateStudentOnAdd` function call preceded by a `TryCatch` block.
The `TryCatch` block is what I call Exception Noise Cancellation pattern, which we will discuss soon in this very section.

But the validation function ensures each and every property in the incoming data is validated before passing it forward to the primitive broker operation, which is the `InsertStudentAsync` in this very instance.
Expand All @@ -70,9 +70,11 @@ Services strongly ensure the single responsibility principle is implemented by n

This rule doesn't necessarily apply to support brokers like `DateTimeBroker` or `LoggingBroker` since they don't specifically target any particular business entity and they are almost generic across the entire system.

For instance, a `StudentService` may integrate with a `StorageBroker` as long as it only targets only the functionality offered by the partial class in the `StorageBroker.Students.cs` file.
For instance, a `StudentService` may integrate with a `StorageBroker` as long as it only targets the functionality offered by the partial class of the `StorageBroker.Students.cs` file when exists.

Foundation services should not integrate with more than one entity broker of any kind simply because it will increase the complexity of validation and orchestration which goes beyond the main purpose of the service which is just simply validation. We push this responsibility further to the orchestration-type services to handle it.
Foundation services should not integrate with more than one entity broker of any kind simply because it will increase the complexity of validation and orchestration of which goes beyond the main purposes the service which is just simply validation.

We push this responsibility further to the orchestration-type services to handle.

### 2.1.2.2 Business Language

Expand Down Expand Up @@ -116,7 +118,7 @@ public async ValueTask<Student> UpsertStudentAsync(Student student)
```

The contract between a processing or an orchestration service and a foundation service will always be the same irregardless of what type of implementation or what type of brokers the foundation service is using.
For example, `AddStudentAsync` could be a call to a database or an API endpoint or simply putting a message on a queue. It all doesn't impact in any way, shape or form the upstream processing service implementation. Here's an example of three different implementations of a foundation service that wouldn't change anything in the implementation of it's upstream services:
For example, `AddStudentAsync` could be a call to a database or an API endpoint or simply putting a message on a queue. It doesn't impact in any way, shape or form the upstream processing service implementation. Here's an example of three different implementations of a foundation service that wouldn't change anything in the implementation of it's upstream services:

With a storage broker:

Expand Down Expand Up @@ -164,7 +166,7 @@ In all of these above cases, the underlying implementation may change, but the e

#### 2.1.3.0.1 Implementation

Let's talk about a real-life example of implementing a simple `Add` function in a foundation service. Let's assume we have the following contract for our `StudentService`:
Let's talk about a real-life example of implementing a simple `Add` function in a foundation service. This is also called our Happy Path test. It is generally the starting point of most CRUD type foundation services. Let's assume we have the following contract for our `StudentService`:

```csharp
public IStudentService
Expand Down Expand Up @@ -206,15 +208,15 @@ private async Task ShouldAddStudentAsync()

In the above test, we defined four variables with the same value. Each variable contains a name that best fits the context it will be used in. For instance, `inputStudent` best fits in the input parameter position, while `storageStudent` best fits to what gets returned from the storage broker after a student is persisted sucessfully.

We also randomize our inputs and outputs across all tests to ensure that the test is targetting a certain functional behavior. There are cases where testing for a specific value or set of values is required. But it will need to have quite a strong justification to do such a thing.
We also randomize our inputs and outputs across all tests to ensure that the test is targeting a certain functional behavior. There are cases where testing for a specific value or set of values is required. But it will need to have quite a strong justification to do such a thing.

It's easy to "fool" tests into reporting a successful operation by using specific input parameters or output values. Randomization is required by default unless needed otherwise. For instance, if you have a function `Add(x, y)` if the test is passing specifically `1` and `2` to the function and expects `3` as a return value, then anyone could ignore the actual arithmatic operation and just return `3` all the time from target function. Take that at a larger enterprise level scale as the problem gets more complex and you can see how important and crucial it is to randomize inputs and outputs by default.

You will also notice that we deep cloned the `expectedStudent` variable to ensure no modifications have happened to the originally passed in student. For instance, assume an input student value has changed for any of it's attributes internally within the `AddStudentAsync` function. That change won't trigger a failing test unless we dereference the `expectedStudent` variable from the input and returned variables.
You will also notice that we use a DeepCloner(force-net) method to deep clone the `expectedStudent` variable to ensure no modifications have happened to the originally passed in student. For instance, assume an input student value has changed for any of it's attributes internally within the `AddStudentAsync` function. That change won't trigger a failing test unless we dereference the `expectedStudent` variable from the input and returned variables.

We mock the response from the storage broker and execute our subject of test `AddStudentAsync` then we verify the returned student value `actualStudent` matches the expected value `expectedStudent` regardless of the reference.

Finally, we verify all calls are done properly and no additional calls has been made to any of the service dependencies.
Finally, we verify all calls are done properly and no additional calls have been made to any of the service dependencies.

Let's make that test pass by writing in an implementation that only satisfies the requirements of the aforementioned test:

Expand Down Expand Up @@ -251,16 +253,16 @@ Our `AddStudentAsync` function in this scenario is now required to validate whet
public Student AddStudentAsync(Student student) =>
TryCatch(async () =>
{
ValidateStudent(student);
ValidateStudentOnAdd(student);

return await this.storageBroker.InsertStudentAsync(student);
});
```

The statement in focus here is `ValidateStudent` function and what it does. Here's an example of how that routine would be implemented:
The statement in focus here is `ValidateStudentOnAdd` function and what it does. Here's an example of how that routine would be implemented:

```csharp
private static void ValidateStudent(Student student)
private static void ValidateStudentOnAdd(Student student)
{
if (student is null)
{
Expand Down Expand Up @@ -342,7 +344,7 @@ public class InvalidStudentException : Xeption
}
```

Every custom exception whether it is localized or categorized should essentially adhere to a full anemic model configuration. This will enforce a strong binding of the exception messages and the testing messages, mainly ensuring messages communicated through the exceptions are the proper messages.
Every custom exception whether it is localized or categorized should essentially adhere to our anemic model principle. This will enforce a strong binding of the exception messages and the testing messages, mainly ensuring messages communicated through the exceptions are the proper messages.

But with continuous validations, the process of collecting these errors conveys more than just a special exception implementation. We will discuss more on this in the next section.

Expand All @@ -356,13 +358,13 @@ In a scenario where we want to ensure any given Id is valid, a dynamic continuou
private static dynamic IsInvalid(Guid id) => new
{
Condition = id == Guid.Empty,
Message = "Id is required"
Message = "Id is invalid"
};
```

Now our Rule doesn't just report whether a particular attribute is invalid or not. It also has a meaningful human-readable message that helps the consumer of the service understand what makes that very attribute invalid.

It's really important to point out the language engineers must use for validation messages. It will all depend on the potential consumers of your system. A non-engineer will not understand a message such as `Text cannot be null, empty or whitespace` - `null` as a term isn't something that is very commonly used. Engineers must work closely with their meatware (The people using the system) to ensure the language makes sense to them.
It's really important to point out the language engineers must use for validation messages. It will all depend on the potential consumers of your system. A non-engineer will not understand a message such as `Text cannot be null, empty or whitespace` - `null` as a term isn't something that is very commonly used. Engineers must work closely with their consumer or advocates for the people using the system to ensure the language makes sense to them.

Dynamic rules by design will allow engineers to modify both their inputs and outputs without breaking any existing functionality as long as `null` values are considered across the board. Here's another manifestation of a Dynamic Validation Rule:

Expand Down Expand Up @@ -405,7 +407,7 @@ private void Validate(params (dynamic Rule, string Parameter)[] validations)
The above function now will take any number of validation rules, and the parameters the rule is running against then examine the conditions and upsert the report of errors. This is how we can use the method above:

```csharp
private void ValidateStudent(Student student)
private void ValidateStudentOnAdd(Student student)
{
Validate(
(Rule: IsInvalid(student.Id), Parameter: nameof(Student.Id)),
Expand Down Expand Up @@ -447,7 +449,7 @@ In this case, we can't validate `Address` as a property on the `Student` level a
In this case we would need a hybrid approach as follows:

```csharp
private void ValidateStudent(Student student)
private void ValidateStudentOnAdd(Student student)
{
Validate(
(Rule: IsInvalid(student.Id), Parameter: nameof(Student.Id)),
Expand All @@ -472,7 +474,7 @@ Validations are three different layers. the first of these layers is the structu

For instance, a property of type `String` should not be empty, `null` or white space. Another example would be for an input parameter of an `int` type, it should not be at it's `default` state which is `0` when trying to enter an age for instance.

The structural validations ensure that the data is in a good shape before moving forward with any further validations - for instance, we can't possibly validate students that has the minimum number of characters (which is a logical validation) in their names if their first name is structurally invalid by being `null`, empty or whitespace.
The structural validations ensure that the data is in a good shape before moving forward with any further validations - for instance, we can't possibly validate students that have the minimum number of characters (which is a logical validation) in their names if their first name is structurally invalid by being `null`, empty or whitespace.

Structural validations play the role of identifying the _required_ properties on any given model, and while a lot of technologies offer the validation annotations, plugins or libraries to globally enforce data validation rules, I choose to perform the validation programmatically and manually to gain more control of what would be required and what wouldn't in a TDD fashion.

Expand Down Expand Up @@ -513,7 +515,7 @@ We want to validate that the student Id is not a structurally invalid Id - such

```csharp
[Fact]
private async void ShouldThrowValidationExceptionOnRegisterWhenIdIsInvalidAndLogItAsync()
private async void ShouldThrowValidationExceptionOnAddWhenIdIsInvalidAndLogItAsync()
{
// given
Student randomStudent = CreateRandomStudent();
Expand All @@ -535,8 +537,8 @@ private async void ShouldThrowValidationExceptionOnRegisterWhenIdIsInvalidAndLog
innerException: invalidStudentException);

// when
ValueTask<Student> registerStudentTask =
this.studentService.RegisterStudentAsync(inputStudent);
ValueTask<Student> addStudentTask =
this.studentService.AddStudentAsync(inputStudent);

StudentValidationException actualStudentValidationException =
await Assert.ThrowsAsync<StudentValidationException>(
Expand Down Expand Up @@ -619,7 +621,7 @@ Here's how an Id validation would look like:

```csharp

private void ValidateStudent(Student student)
private void ValidateStudentOnAdd(Student student)
{
Validate((Rule: IsInvalid(student.Id), Parameter: nameof(Student.Id)));
}
Expand Down Expand Up @@ -653,15 +655,15 @@ That's the reason why we decided to implement a private static method `IsInvalid

The purpose of the `ValidateStudent` method is to simply set up the rules and take an action by throwing an exception if any of these rules are violated. There's always an opportunity to aggregate the violation errors rather than throwing too early at the first sign of structural or logical validation issue to be detected.

Now, with the implementation above, we need to call that method to structurally and logically validate our input. Let's make that call in our `RegisterStudentAsync` method as follows:
Now, with the implementation above, we need to call that method to structurally and logically validate our input. Let's make that call in our `AddStudentAsync` method as follows:

###### StudentService.cs

```csharp
public ValueTask<Student> RegisterStudentAsync(Student student) =>
public ValueTask<Student> AddStudentAsync(Student student) =>
TryCatch(async () =>
{
ValidateStudent(student);
ValidateStudentOnAdd(student);

return await this.storageBroker.InsertStudentAsync(student);
});
Expand Down Expand Up @@ -736,7 +738,7 @@ Our test to validate these values logically would be as follows:

```csharp
[Fact]
private async Task ShouldThrowValidationExceptionOnRegisterIfUpdatedByNotSameAsCreatedByAndLogItAsync()
private async Task ShouldThrowValidationExceptionOnAddIfUpdatedByNotSameAsCreatedByAndLogItAsync()
{
// given
Student randomStudent = CreateRandomStudent();
Expand All @@ -758,7 +760,7 @@ private async Task ShouldThrowValidationExceptionOnRegisterIfUpdatedByNotSameAsC

// when
ValueTask<Student> registerStudentTask =
this.studentService.RegisterStudentAsync(inputStudent);
this.studentService.AddStudentAsync(inputStudent);

StudentValidationException actualStudentValidationException =
await Assert.ThrowsAsync<StudentValidationException>(
Expand Down Expand Up @@ -794,7 +796,7 @@ Just like we did in the structural validations section, we are going to validate
###### StudentService.Validations.cs

```csharp
private void ValidateStudent(Student student)
private void ValidateStudentIsNotSame(Student student)
{
Validate(
(Rule: IsNotSame(
Expand All @@ -816,7 +818,9 @@ private static dynamic IsNotSame(

private void Validate(params (dyanamic Rule, string Parameter)[] validations)
{
var invalidStudentException = new Exception();
var invalidStudentException =
new InvalidStudentException(
message: "Student is invalid. Please fix the errors and try again.");

foreach((dynamic rule, string parameter) in validations)
{
Expand All @@ -827,6 +831,8 @@ private void Validate(params (dyanamic Rule, string Parameter)[] validations)
value: rule.Message);
}
}

invalidStudentException.ThrowIfContainsErrors();
}
```

Expand Down Expand Up @@ -928,7 +934,7 @@ private static void VerifyStudentExists(Student maybeStudent, Guid studentId)
if (maybeStudent is null)
{
throw new NotFoundStudentException(
message: $"Couldn't find student with id: {studentId}.");
message: $"Could not find student with id: {studentId}.");
}
}
```
Expand Down Expand Up @@ -1012,7 +1018,7 @@ our unit test to validate a `DependencyValidation` exception would be thrown in

```csharp
[Fact]
private async void ShouldThrowDependencyValidationExceptionOnRegisterIfStudentAlreadyExistsAndLogItAsync()
private async void ShouldThrowDependencyValidationExceptionOnAddIfStudentAlreadyExistsAndLogItAsync()
{
// given
Student someStudent = CreateRandomStudent();
Expand All @@ -1037,7 +1043,7 @@ private async void ShouldThrowDependencyValidationExceptionOnRegisterIfStudentAl

// when
ValueTask<Student> registerStudentTask =
this.studentService.RegisterStudentAsync(inputStudent);
this.studentService.AddStudentAsync(inputStudent);

StudentDependencyValidationException actualStudentDependencyValidationException =
await Assert.ThrowsAsync<StudentDependencyValidationException>(
Expand Down

0 comments on commit e8e368a

Please sign in to comment.