diff --git a/1. Brokers/1. Brokers.md b/1. Brokers/1. Brokers.md index 37b12ed..628ec30 100644 --- a/1. Brokers/1. Brokers.md +++ b/1. Brokers/1. Brokers.md @@ -32,22 +32,22 @@ Brokers must satisfy a local contract and implement a local interface to allow d For instance, given that we have a local contract, `IStorageBroker` that requires an implementation for any given CRUD operation for a local model `Student` - the contract operation would be as follows: ```csharp - public partial interface IStorageBroker - { - ValueTask> SelectAllStudentsAsync(); - } +public partial interface IStorageBroker +{ + ValueTask> SelectAllStudentsAsync(); +} ``` An implementation for a storage broker would be as follows: ```csharp - public partial class StorageBroker - { - public DbSet Students { get; set; } +public partial class StorageBroker +{ + public DbSet Students { get; set; } - public async ValueTask> SelectAllStudentsAsync() => - await SelectAllAsync(); - } + public async ValueTask> SelectAllStudentsAsync() => + await SelectAllAsync(); +} ``` A local contract implementation can be replaced at any point, from utilizing the Entity Framework as shown in the previous example to using a completely different technology like Dapper or an entirely different infrastructure like an Oracle or PostgreSQL database. @@ -57,8 +57,8 @@ Brokers should not have any form of flow control, such as if statements, while l For instance, a broker method that retrieves a list of students from a database would look something like this: ```csharp - public async ValueTask> SelectAllStudentsAsync() => - await SelectAllAsync(); +public async ValueTask> SelectAllStudentsAsync() => + await SelectAllAsync(); ``` A simple function that calls the native EntityFramework `DbSet` and returns a local model like `Student`. @@ -73,26 +73,26 @@ Brokers are also required to handle their configurations - they may have a depen For instance, connection strings in database communications are required to be retrieved and passed into the database client to establish a successful connection, as follows: ```csharp - public partial class StorageBroker : EFxceptionsContext, IStorageBroker - { - private readonly IConfiguration configuration; +public partial class StorageBroker : EFxceptionsContext, IStorageBroker +{ + private readonly IConfiguration configuration; - public StorageBroker(IConfiguration configuration) - { - this.configuration = configuration; - this.Database.Migrate(); - } + public StorageBroker(IConfiguration configuration) + { + this.configuration = configuration; + this.Database.Migrate(); + } - ... - ... + ... + ... - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); - string connectionString = this.configuration.GetConnectionString("DefaultConnection"); - optionsBuilder.UseSqlServer(connectionString); - } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); + string connectionString = this.configuration.GetConnectionString("DefaultConnection"); + optionsBuilder.UseSqlServer(connectionString); } +} ``` ### 1.2.4 Natives from Primitives @@ -128,8 +128,8 @@ If a broker supports an API endpoint, it shall follow the RESTFul semantics, suc ```csharp - public async ValueTask PostStudentAsync(Student student) => - await this.PostAsync(RelativeUrl, student); +public async ValueTask PostStudentAsync(Student student) => + await this.PostAsync(RelativeUrl, student); ``` @@ -343,60 +343,60 @@ namespace OtripleS.Web.Api.Brokers.Storages #### For IDateTimeBroker.cs: ```csharp public interface IDateTimeBroker - { - ValueTask GetCurrentDateTimeOffsetAsync(); - } +{ + ValueTask GetCurrentDateTimeOffsetAsync(); +} ``` #### For DateTimeBroker.cs: ```csharp public class DateTimeBroker : IDateTimeBroker - { - public async ValueTask GetCurrentDateTimeOffsetAsync() => - DateTimeOffset.UtcNow; - } +{ + public async ValueTask GetCurrentDateTimeOffsetAsync() => + DateTimeOffset.UtcNow; +} ``` #### For ILoggingBroker.cs: ```csharp public interface ILoggingBroker - { - ValueTask LogInformationAsync(string message); - ValueTask LogTraceAsync(string message); - ValueTask LogDebugAsync(string message); - ValueTask LogWarningAsync(string message); - ValueTask LogErrorAsync(Exception exception); - ValueTask LogCriticalAsync(Exception exception); - } +{ + ValueTask LogInformationAsync(string message); + ValueTask LogTraceAsync(string message); + ValueTask LogDebugAsync(string message); + ValueTask LogWarningAsync(string message); + ValueTask LogErrorAsync(Exception exception); + ValueTask LogCriticalAsync(Exception exception); +} ``` #### For LoggingBroker.cs: ```csharp public class LoggingBroker : ILoggingBroker - { - private readonly ILogger logger; +{ + private readonly ILogger logger; - public LoggingBroker(ILogger logger) => - this.logger = logger; + public LoggingBroker(ILogger logger) => + this.logger = logger; - public async ValueTask LogInformationAsync(string message) => - this.logger.LogInformation(message); + public async ValueTask LogInformationAsync(string message) => + this.logger.LogInformation(message); - public async ValueTask LogTraceAsync(string message) => - this.logger.LogTrace(message); + public async ValueTask LogTraceAsync(string message) => + this.logger.LogTrace(message); - public async ValueTask LogDebugAsync(string message) => - this.logger.LogDebug(message); + public async ValueTask LogDebugAsync(string message) => + this.logger.LogDebug(message); - public async ValueTask LogWarningAsync(string message) => - this.logger.LogWarning(message); + public async ValueTask LogWarningAsync(string message) => + this.logger.LogWarning(message); - public async ValueTask LogErrorAsync(Exception exception) => - this.logger.LogError(exception.Message, exception); + public async ValueTask LogErrorAsync(Exception exception) => + this.logger.LogError(exception.Message, exception); - public async ValueTask LogCriticalAsync(Exception exception) => - this.logger.LogCritical(exception, exception.Message); - } + public async ValueTask LogCriticalAsync(Exception exception) => + this.logger.LogCritical(exception, exception.Message); +} ``` ## 1.6 Summary diff --git a/2. Services/2.1 Foundations/2.1 Foundations.md b/2. Services/2.1 Foundations/2.1 Foundations.md index 1c9f951..cbbcd89 100644 --- a/2. Services/2.1 Foundations/2.1 Foundations.md +++ b/2. Services/2.1 Foundations/2.1 Foundations.md @@ -15,7 +15,7 @@ For instance, if a storage broker is offering `InsertStudentAsync(Student studen ```csharp public async ValueTask AddStudentAsync(Student student) { - ValidateStudentOnAdd(student); + await ValidateStudentOnAddAsync(student); return await this.storageBroker.InsertStudentAsync(student); } @@ -53,7 +53,7 @@ But they offer a validation and exception handling (and mapping) wrapper around public ValueTask AddStudentAsync(Student student) => TryCatch(async () => { - ValidateStudentOnAdd(student); + await ValidateStudentOnAddAsync(student); return await this.storageBroker.InsertStudentAsync(student); }); @@ -250,19 +250,19 @@ await this.studentService.AddStudentAsync(noStudent); Our `AddStudentAsync` function in this scenario is now required to validate whether the passed in parameter is `null` or not before going any further with any other type of validations or the business logic itself. Something like this: ```csharp -public Student AddStudentAsync(Student student) => +public ValueTask AddStudentAsync(Student student) => TryCatch(async () => { - ValidateStudentOnAdd(student); + await ValidateStudentOnAddAsync(student); return await this.storageBroker.InsertStudentAsync(student); }); ``` -The statement in focus here is `ValidateStudentOnAdd` function and what it does. Here's an example of how that routine would be implemented: +The method in focus here is `ValidateStudentIsNotNull` which is called by the `ValidateStudentOnAddAsync` validations engine. Here's an example of how that routine would be implemented: ```csharp -private static void ValidateStudentOnAdd(Student student) +private static void ValidateStudentIsNotNull(Student student) { if (student is null) { @@ -355,7 +355,7 @@ A non-circuit-breaking or continuous validation process will require the ability In a scenario where we want to ensure any given Id is valid, a dynamic continuous validation rule would look something like this: ```csharp -private static dynamic IsInvalid(Guid id) => new +private static async ValueTask IsInvalidAsync(Guid id) => new { Condition = id == Guid.Empty, Message = "Id is invalid" @@ -369,7 +369,7 @@ It's really important to point out the language engineers must use for validatio 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: ```csharp -private static dynamic IsNotSame( +private static async ValueTask IsNotSameAsync( Guid firstId, Guid secondId, string secondIdName) => new @@ -407,12 +407,14 @@ 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 ValidateStudentOnAdd(Student student) +private async ValueTask ValidateStudentOnAddAsync(Student student) { + ...... + Validate( - (Rule: IsInvalid(student.Id), Parameter: nameof(Student.Id)), - (Rule: IsInvalid(student.Name), Parameter: nameof(Student.Name)), - (Rule: IsInvalid(student.Grade), Parameter: nameof(Student.Grade)) + (Rule: await IsInvalidAsync(student.Id), Parameter: nameof(Student.Id)), + (Rule: await IsInvalidAsync(student.Name), Parameter: nameof(Student.Name)), + (Rule: await IsInvalidAsync(student.Grade), Parameter: nameof(Student.Grade)) ); } ``` @@ -449,18 +451,22 @@ 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 ValidateStudentOnAdd(Student student) +private async ValueTask ValidateStudentOnAddAsync(Student student) { + ...... + Validate( - (Rule: IsInvalid(student.Id), Parameter: nameof(Student.Id)), - (Rule: IsInvalid(student.Name), Parameter: nameof(Student.Name)), - (Rule: IsInvalid(student.Address), Parameter: nameof(Student.Address)) + (Rule: await IsInvalidAsync(student.Id), Parameter: nameof(Student.Id)), + (Rule: await IsInvalidAsync(student.Name), Parameter: nameof(Student.Name)), + (Rule: await IsInvalidAsync(student.Address), Parameter: nameof(Student.Address)) ); + ...... + Validate( - (Rule: IsInvalid(student.Address.Street), Parameter: nameof(StudentAddress.Street)), - (Rule: IsInvalid(student.Address.City), Parameter: nameof(StudentAddress.City)), - (Rule: IsInvalid(student.Address.ZipCode), Parameter: nameof(StudentAddress.ZipCode)) + (Rule: await IsInvalidAsync(student.Address.Street), Parameter: nameof(StudentAddress.Street)), + (Rule: await IsInvalidAsync(student.Address.City), Parameter: nameof(StudentAddress.City)), + (Rule: await IsInvalidAsync(student.Address.ZipCode), Parameter: nameof(StudentAddress.ZipCode)) ); } ``` @@ -595,13 +601,13 @@ public class StudentValidationException : Xeption The string messaging for the outer-validation above will be passed when the exception is initialized from the service class as shown below. ```csharp -private StudentValidationException CreateAndLogValidationException(Xeption exception) +private async ValueTask CreateAndLogValidationExceptionAsync(Xeption exception) { var studentValidationException = new StudentValidationException( message: "Student validation error occurred, please check your input and try again.", innerException: exception); - this.loggingBroker.LogErrorAsync(studentValidationException); + await this.loggingBroker.LogErrorAsync(studentValidationException); return studentValidationException; } @@ -621,15 +627,17 @@ Here's how an Id validation would look like: ```csharp -private void ValidateStudentOnAdd(Student student) +private async ValueTask ValidateStudentOnAddAsync(Student student) { - Validate((Rule: IsInvalid(student.Id), Parameter: nameof(Student.Id))); + ...... + + Validate((Rule: await IsInvalidAsync(student.Id), Parameter: nameof(Student.Id))); } -private static dynamic IsInvalid(Guid id) => new +private static async ValueTask IsInvalidAsync(Guid id) => new { Condition = id == Guid.Empty, - Message = "Id is required" + Message = "Id is invalid" }; private void Validate(params (dynamic Rule, string Parameter)[] validations) @@ -663,7 +671,7 @@ Now, with the implementation above, we need to call that method to structurally public ValueTask AddStudentAsync(Student student) => TryCatch(async () => { - ValidateStudentOnAdd(student); + await ValidateStudentOnAddAsync(student); return await this.storageBroker.InsertStudentAsync(student); }); @@ -690,11 +698,11 @@ private async ValueTask TryCatch(ReturningStudentFunction returningStud } catch (InvalidStudentException invalidStudentInputException) { - throw CreateAndLogValidationException(invalidStudentInputException); + throw await CreateAndLogValidationExceptionAsync(invalidStudentInputException); } } -private StudentValidationException CreateAndLogValidationException(Xeption exception) +private async ValueTask CreateAndLogValidationExceptionAsync(Xeption exception) { var studentValidationException = new StudentValidationException( message: "Student validation error occurred, please check your input and try again.", @@ -717,7 +725,8 @@ In a `TryCatch` method, we can add as many inner and external exceptions as we w Logical validations are the second in order to structural validations. Their main responsibility by definition is to logically validate whether a structurally valid piece of data is logically valid. For instance, a date of birth for a student could be structurally valid by having a value of `1/1/1800` but logically, a student that is over 200 years of age is an impossibility. -The most common logical validations are validations for audit fields such as `CreatedBy` and `UpdatedBy` - it's logically impossible that a new record can be inserted with two different values for the authors of that new record - simply because data can only be inserted by one person at a time. +The most common logical validations are validations for audit fields such as `CreatedBy`, `UpdatedBy`, `CreatedDate`, and `UpdatedDate` - it's logically impossible a new record can be inserted with two different values for the authors of that new record - simply because data can only be inserted by one person at a time. +The same goes for the `CreatedDate` and `UpdatedDate` fields - it's logically impossible for a record to be created and updated at the same exact times. Let's talk about how we can test-drive and implement logical validations: @@ -728,22 +737,31 @@ In the common case of testing logical validations for audit fields, we want to t Let's assume our Student model looks as follows: ```csharp -public class Student { - Guid CreatedBy {get; set;} - Guid UpdatedBy {get; set;} +public class Student +{ + String CreatedBy {get; set;} + String UpdatedBy {get; set;} + DateTimeOffset CreatedDate {get; set;} + DateTimeOffset UpdatedDate {get; set;} } ``` +Our two tests to validate these values logically would be as follows: -Our test to validate these values logically would be as follows: +In this first test example, we would ensure that the `UpdatedBy` field is not the same as the `CreatedBy` field. ```csharp [Fact] -private async Task ShouldThrowValidationExceptionOnAddIfUpdatedByNotSameAsCreatedByAndLogItAsync() +private async Task ShouldThrowValidationExceptionOnAddIfAuditPropertiesIsNotTheSameAndLogItAsync() { // given - Student randomStudent = CreateRandomStudent(); - Student inputStudent = randomStudent; - inputStudent.UpdatedBy = Guid.NewGuid(); + DateTimeOffset randomDateTime = GetRandomDateTimeOffset(); + DateTImeOffset now = randomDateTime; + Student randomStudent = CreateRandomStudent(now); + Student invalidStudent = randomStudent; + invalidStudent.CreatedBy = GetRandomString(); + invalidStudent.UpdatedBy = GetRandomString(); + invalidStudent.CreatedDate = now; + invalidStudent.UpdatedDate = GetRandomDateTimeOffset(); var invalidStudentException = new InvalidStudentException( @@ -751,13 +769,21 @@ private async Task ShouldThrowValidationExceptionOnAddIfUpdatedByNotSameAsCreate invalidStudentException.AddData( key: nameof(Student.UpdatedBy), - value: $"Id is not the same as {nameof(Student.CreatedBy)}."); + value: $"Text is not the same as {nameof(Student.CreatedBy)}."); + + invalidStudentException.AddData( + key: nameof(Student.UpdatedDate), + value: $"Date is not the same as {nameof(Student.CreatedDate)}."); var expectedStudentValidationException = new StudentValidationException( message: "Student validation error occurred, fix errors and try again.", innerException: invalidStudentException); + this.dateTimeBrokerMock.Setup(broker => + broker.GetCurrentDateTimeOffsetAsync()) + .ReturnsAsync(now); + // when ValueTask addStudentTask = this.studentService.AddStudentAsync(inputStudent); @@ -770,6 +796,10 @@ private async Task ShouldThrowValidationExceptionOnAddIfUpdatedByNotSameAsCreate actualStudentValidationException.Should().BeEquivalentTo( expectedStudentValidationException); + this.dateTimeBrokerMock.Verify(broker => + broker.GetCurrentDateTimeOffsetAsync(), + Times.Once); + this.loggingBrokerMock.Verify(broker => broker.LogError(It.Is(SameExceptionAs( expectedStudentValidationException))), @@ -779,44 +809,210 @@ private async Task ShouldThrowValidationExceptionOnAddIfUpdatedByNotSameAsCreate broker.InsertStudentAsync(It.IsAny()), Times.Never); - this.loggingBrokerMock.VerifyNoOtherCalls(); this.dateTimeBrokerMock.VerifyNoOtherCalls(); + this.loggingBrokerMock.VerifyNoOtherCalls(); this.storageBrokerMock.VerifyNoOtherCalls(); } ``` -In the above test, we have changed the value of the `UpdatedBy` field to ensure it completely differs from the `CreatedBy` field - now we expect an `InvalidStudentException` with the `CreatedBy` to be the reason for this validation exception to occur. +In the above test, we have changed the value of the `UpdatedBy` field to ensure it completely differs from the `CreatedBy` field - now we expect an `InvalidStudentException` with the `CreatedBy` to be the reason for this validation exception to occur. + +```csharp +[Theory] +[InlineData(1)] +[InlineData(-61)] +public async Task ShouldThrowValidationExceptionOnAddIfCreatedDateIsNotRecentAndLogItAsync( + int invalidSeconds) +{ + // given + DateTimeOffset randomDateTime = + GetRandomDateTimeOffset(); + + DateTimeOffset now = randomDateTime; + Student randomStudent = CreateRandomStudent(); + Student invalidStudent = randomStudent; + + DateTimeOffset invalidDate = + now.AddSeconds(invalidSeconds); + + invalidStudent.CreatedDate = invalidDate; + invalidStudent.UpdatedDate = invalidDate; + + var invalidStudentException = new InvalidStudentException( + message: "Student is invalid, fix the errors and try again."); + + invalidStudentException.AddData( + key: nameof(Student.CreatedDate), + values: $"Date is not recent"); + + var expectedStudentValidationException = + new StudentValidationException( + message: "Student validation error occurred, fix errors and try again.", + innerException: invalidSourceException); + + this.dateTimeBrokerMock.Setup(broker => + broker.GetCurrentDateTimeOffsetAsync()) + .ReturnsAsync(now); + + // when + ValueTask addStudentTask = + this.studentService.AddStudentAsync(invalidSource); + + StudentValidationException actualStudentValidationException = + await Assert.ThrowsAsync( + addStudentTask.AsTask); + + // then + actualStudentValidationException.Should().BeEquivalentTo( + expectedStudentValidationException); + + this.dateTimeBrokerMock.Verify(broker => + broker.GetCurrentDateTimeOffsetAsync(), + Times.Once); + + this.loggingBrokerMock.Verify(broker => + broker.LogErrorAsync(It.Is( + SameExceptionAs(expectedSourceValidationException))), + Times.Once); + + this.storageBrokerMock.Verify(broker => + broker.InsertSourceAsync(It.IsAny()), + Times.Never); + + this.dateTimeBrokerMock.VerifyNoOtherCalls(); + this.loggingBrokerMock.VerifyNoOtherCalls(); + this.storageBrokerMock.VerifyNoOtherCalls(); +} +``` + +In the test example want to ensure that the `CreatedDate` is a date current to 60 seconds before or after. We would not want to allow dates that are not recent on an `AddStudent` operation. We then expect an `InvalidStudentException` with the `CreatedDate` to be the reason for this validation exception to occur. -Let's go ahead an write an implementation for this failing test. +Let's go ahead an write the implementations for these failing tests. ##### 2.1.3.1.3.1 Implementing Logical Validations -Just like we did in the structural validations section, we are going to validate a logical rule as follows: +Just like we did in the structural validations section, we are going to validate our logical rule(s) as follows: ###### StudentService.Validations.cs ```csharp -private void ValidateStudentIsNotSame(Student student) + + private async ValueTask ValidateStudentOnAddAsync(Source student) { - Validate( - (Rule: IsNotSame( - firstId: student.UpdatedBy, - secondId: student.CreatedBy, - secondIdName: nameof(student.CreatedBy)), - Parameter: nameof(Student.UpdatedBy)) - ); + ValidateStudentIsNotNull(student); + + Validate( + (Rule: await IsInvalidAsync(student.Id), Parameter: nameof(Student.Id)), + (Rule: await IsInvalidAsync(student.Name), Parameter: nameof(Student.Name)), + (Rule: await IsInvalidAsync(student.CreatedBy), Parameter: nameof(Student.CreatedBy)), + (Rule: await IsInvalidAsync(student.UpdatedBy), Parameter: nameof(Student.UpdatedBy)), + (Rule: await IsInvalidAsync(student.CreatedDate), Parameter: nameof(Student.CreatedDate)), + (Rule: await IsInvalidAsync(student.UpdatedDate), Parameter: nameof(Student.UpdatedDate)), + + (Rule: await IsValuesNotSameAsync( + createBy: student.UpdatedBy, + updatedBy: student.CreatedBy, + createdByName: nameof(Student.CreatedBy)), + + Parameter: nameof(Student.UpdatedBy)), + + (Rule: await IsDatesNotSameAsync( + createdDate: student.CreatedDate, + updatedDate: studente.UpdatedDate, + nameof(Studente.CreatedDate)), + + Parameter: nameof(Student.UpdatedDate)), + + (Rule: await IsNotRecentAsync(student.CreatedDate), Parameter: nameof(Student.CreatedDate))); } +``` -private static dynamic IsNotSame( - Guid firstId, - Guid secondId, - string secondIdName) => new +In the above implementation, we have implemented our rule validation engine method to validate the student object for the OnAdd operation, with a compilation of all the rules we need to setup to validate structurally and logically the student input object. +We then call the logical validation methods `IsInvalidAsync`, `IsValuesNotSameAsync`, `IsDatesNotSameAsync` and `IsNotRecentAsync` to asure are conditional requirements are met. Here are the example implementations for these methods: + +#### For `IsInvalidAsync` +```csharp +private async ValueTask IsInvalidAsync(Guid id) => new +{ + Condition = id == Guid.Empty, + Message = "Id is invalid" +}; + +private async ValueTask IsInvalidAsync(string name) => new +{ + Condition = String.IsNullOrWhiteSpace(name), + Message = "Text is required" +}; + +private async ValueTask IsInvalidAsync(DateTimeOffset date) => new +{ + Condition = date == default, + Message = "Date is invalid" +}; +``` + +#### For `IsValuesNotSameAsync` +```csharp +private async ValueTask IsValuesNotSameAsync( + string createBy, + string updatedBy, + string createdByName) => new { - Condition = firstId != secondId, - Message = $"Id is not the same as {secondIdName}." + Condition = createBy != updatedBy, + Message = $"Text is not the same as {createdByName}" }; +``` + +#### For `IsDatesNotSameAsync` +```csharp +private async ValueTask IsDatesNotSame( + DateTimeOffset createdDate, + DateTimeOffset updatedDate, + string createdDateName) => new + { + Condition = createdDate != updatedDate, + Message = $"Date is not the same as {createdDateName}" + }; +``` + +#### For `IsNotRecentAsync` +```csharp +private async ValueTask IsNotRecentAsync(DateTimeOffset date) +{ + var (isNotRecent, startDate, endDate) = await IsDateNotRecentAsync(date); + + return new + { + Condition = isNotRecent, + Message = $"Date is not recent. Expected a value between {startDate} and {endDate} but found {date}" + }; +}; + +private async ValueTask<(bool IsNotRecent, DateTimeOffset StartDate, DateTimeOffset EndDate)> + IsDateNotRecentAsync(DateTimeOffset date) +{ + int pastSeconds = 60; + int futureSeconds = 0; + + DateTimeOffset currentDateTime = + await this.dateTimeBroker.GetCurrentDateTimeOffsetAsync(); -private void Validate(params (dyanamic Rule, string Parameter)[] validations) + if (currentDateTime == default) + { + return (false, default, default); + } + + TimeSpan timeDifference = currentDateTime.Subtract(date); + DateTimeOffset startDate = currentDateTime.AddSeconds(-pastSeconds); + DateTimeOffset endDate = currentDateTime.AddSeconds(futureSeconds); + bool isNotRecent = timeDifference.TotalSeconds is > 60 or < 0; + + return (isNotRecent, startDate, endDate); +} +``` +#### Finally our `Validate` method would look like this: +```csharp +private static void Validate(params (dyanamic Rule, string Parameter)[] validations) { var invalidStudentException = new InvalidStudentException( @@ -870,13 +1066,14 @@ The above model is the localization aspect of handling the issue. Now let's writ private async Task ShouldThrowValidationExceptionOnRetrieveByIdIfStudentNotFoundAndLogItAsync() { // given - Guid randomStudentId = Guid.NewGuid(); - Guid inputStudentId = randomStudentId; - Student noStudent = null; + Guid someStudentId = Guid.NewGuid(); + Student nullStudent = null; + var innerException = new Exception() var notFoundStudentException = new NotFoundStudentException( - message: $"Couldn't find a student with id: {inputStudentId}"); + message: $"Student not found with the id: {inputStudentId}", + innerException: innerException.innerException.As()); var expectedStudentValidationException = new StudentValidationException( @@ -934,7 +1131,7 @@ private static void VerifyStudentExists(Student maybeStudent, Guid studentId) if (maybeStudent is null) { throw new NotFoundStudentException( - message: $"Could not find student with id: {studentId}."); + message: $"Student not found with id: {studentId}."); } } ``` @@ -953,17 +1150,17 @@ private async ValueTask TryCatch(ReturningStudentFunction returningStud .. catch (NotFoundStudentException notFoundStudentException) { - throw CreateAndLogValidationException(notFoundStudentException); + throw await CreateAndLogValidationExceptionAsync(notFoundStudentException); } } -private StudentValidationException CreateAndLogValidationException(Xeption exception) +private async ValueTask CreateAndLogValidationExceptionAsync(Xeption exception) { var studentValidationException = new StudentValidationException( - message: "Invalid input, contact support.", + message: "Student validation error occurred, fix errors and try again.", innerException: exception); - this.loggingBroker.LogErrorAsync(studentValidationException); + await this.loggingBroker.LogErrorAsync(studentValidationException); return studentValidationException; } @@ -977,7 +1174,7 @@ The above implementation will take care of categorizing a `NotFoundStudentExcept public ValueTask RetrieveStudentByIdAsync(Guid studentId) => TryCatch(async () => { - ValidateStudentId(studentId); + await ValidateStudentIdAsync(studentId); Student maybeStudent = await this.storageBroker.SelectStudentByIdAsync(studentId); @@ -1018,52 +1215,58 @@ our unit test to validate a `DependencyValidation` exception would be thrown in ```csharp [Fact] -private async void ShouldThrowDependencyValidationExceptionOnAddIfStudentAlreadyExistsAndLogItAsync() +private async Task ShouldThrowDependencyValidationExceptionOnAddIfStudentAlreadyExistsAndLogItAsync() { // given Student someStudent = CreateRandomStudent(); - string someMessage = GetRandomMessage(); - + var duplicateKeyException = - new DuplicateKeyException(exceptionMessage); + new DuplicateKeyException( + message: "Duplicate key error occurred"); var alreadyExistsStudentException = new AlreadyExistsStudentException( - message: "Student with the same id already exists.", - innerException: duplicateKeyException); + message: "Student already exists occurred.", + innerException: duplicateKeyException, + data: duplicateKeyException); var expectedStudentDependencyValidationException = new StudentDependencyValidationException( message: "Student dependency validation error occurred, try again.", innerException: alreadyExistsStudentException); - this.storageBrokerMock.Setup(broker => - broker.InsertStudentAsync(It.IsAny())) + this.dateTimeBroker.Setup(broker => + broker.GetDateTimeOffsetAsync()) .ThrowsAsync(duplicateKeyException); // when ValueTask addStudentTask = - this.studentService.AddStudentAsync(inputStudent); + this.studentService.AddStudentAsync(someStudent); StudentDependencyValidationException actualStudentDependencyValidationException = await Assert.ThrowsAsync( - addStudentTask.AsTask); + testcode: addStudentTask.AsTask); // then actualStudentDependencyValidationException.Should().BeEquivalentTo( expectedStudentDependencyValidationException); - this.storageBrokerMock.Verify(broker => - broker.InsertStudentAsync(It.IsAny()), + this.dateTimeBroker.Verify(broker => + broker.GetDateTimeOffsetAsync(), Times.Once); - + this.loggingBrokerMock.Verify(broker => - broker.LogError(It.Is(SameExceptionAs( + broker.LogErrorAsync(It.Is(SameExceptionAs( expectedStudentDependencyValidationException))), Times.Once); - this.storageBrokerMock.VerifyNoOtherCalls(); + this.storageBrokerMock.Verify(broker => + broker.InsertStudentAsync(It.IsAny()), + Times.Never); + + this.dateTimeBroker.VerifyNoOtherCalls(); this.loggingBrokerMock.VerifyNoOtherCalls(); + this.storageBrokerMock.VerifyNoOtherCalls(); } ``` @@ -1085,16 +1288,23 @@ Which is the case that we have here with our `AlreadyExistsStudentException` and ##### 2.1.3.0.5.1 Implementing Dependency Validations -Depending on where the validation error originates from, the implementation of dependency validations may or may not contain any business logic. As we previously mentioned, if the error is originating from the external resource (which is the case here) - then all we have to do is just wrap that error in a local exception then categorize it with an external exception under dependency validation. +Depending on where the validation error originates from, the implementation of dependency validations may or may not contain any business logic. As we previously mentioned, if the error is originating from the external resource (which is the case here) - the thrown exception carries with it a Data property and we will propagate this data upstream, then all we have to do is just wrap that error in a local exception then categorize it with an external exception under dependency validation. + +To ensure the aforementioned test passed, we are going to need some exception models. -To ensure the aforementioned test passed, we are going to need few models. For the `AlreadyExistsStudentException` the implementation would be as follows: +We also need to bring the innerException Data property to the local exception model to ensure the original message is propagated through the system.(as previously mentioned) + ```csharp public class AlreadyExistsStudentException : Exception { - public AlreadyExistsStudentException(string message, Exception innerException) - : base (message, innerException) {} + public AlreadyExistsStudentException( + string message, + Exception innerException, + IDictionary data) + : base (message, innerException, data) + { } } ``` @@ -1126,23 +1336,25 @@ private async ValueTask TryCatch(ReturningStudentFunction returningStud { var alreadyExistsStudentException = new AlreadyExistsStudentException( - message: "Student with the same Id already exists", - innerException: duplicateKeyException); + message: "Student already exists occurred.", + innerException: duplicateKeyException, + data: duplicateKeyException.Data); - throw CreateAndLogDependencyValidationException(alreadyExistsStudentException); + throw await CreateAndLogDependencyValidationExceptionAsync(alreadyExistsStudentException); } } ... -private StudentDependencyValidationException CreateAndLogDependencyValidationException(Xeption exception) +private async ValueTask CreateAndLogDependencyValidationExceptionAsync( + Xeption exception) { var studentDependencyValidationException = new StudentDependencyValidationException( message: "Student dependency validation error occurred, please try again.", innerException: exception); - this.loggingBroker.LogErrorAsync(studentDependencyValidationException); + await this.loggingBroker.LogErrorAsync(studentDependencyValidationException); return studentDependencyValidationException; } diff --git a/2. Services/2.3 Orchestrations/2.3 Orchestrations.md b/2. Services/2.3 Orchestrations/2.3 Orchestrations.md index 4823efb..3ad0077 100644 --- a/2. Services/2.3 Orchestrations/2.3 Orchestrations.md +++ b/2. Services/2.3 Orchestrations/2.3 Orchestrations.md @@ -692,7 +692,7 @@ private async Task ShouldThrowDependencyValidationExceptionOnCreateIfDependencyV Times.Once); this.libraryCardServiceMock.Verify(service => - service.AddLibraryCard(It.IsAny()), + service.AddLibraryCardAsync(It.IsAny()), Times.Once); this.studentServiceMock.VerifyNoOtherCalls(); @@ -720,30 +720,31 @@ public partial class StudentOrchestrationService } catch (StudentValidationException studentValidationException) { - throw CreateAndLogDependencyValidationException(studentValidationException); + throw await CreateAndLogDependencyValidationExceptionAsync(studentValidationException); } catch (StudentDependencyValidationException studentDependencyValidationException) { - throw CreateAndLogDependencyValidationException(studentDependencyValidationException); + throw await CreateAndLogDependencyValidationExceptionAsync(studentDependencyValidationException); } catch (LibraryCardValidationException libraryCardValidationException) { - throw CreateAndLogDependencyValidationException(libraryCardValidationException); + throw await CreateAndLogDependencyValidationExceptionAsync(libraryCardValidationException); } catch (LibraryCardDependencyValidationException libraryCardDependencyValidationException) { - throw CreateAndLogDependencyValidationException(libraryCardDependencyValidationException); + throw await CreateAndLogDependencyValidationExceptionAsync(libraryCardDependencyValidationException); } } - private StudentOrchestrationDependencyValidationException CreateAndLogDependencyValidationException(Xeption exception) + private async ValueTask + CreateAndLogDependencyValidationExceptionAsync(Xeption exception) { var studentOrchestrationDependencyValidationException = new StudentOrchestrationDependencyValidationException( message: "Student dependency validation error occurred, fix errors and try again", exception.innerException as Xeption); - this.loggingBroker.LogErrorAsync(studentOrchestrationDependencyValidationException); + await this.loggingBroker.LogErrorAsync(studentOrchestrationDependencyValidationException); return studentOrchestrationDependencyValidationException; } @@ -790,7 +791,7 @@ Let's take a look at the possible variants for orchestration services and where | Variant | Dependencies | Consumers | Complexity | |------------------------ |--------------------------- | ------------------------------------------| -----------| -| Orchestrations Services | Foundation or Processing Services | Coordination Services | Low | +| Orchestration Services | Foundation or Processing Services | Coordination Services | Low | | Coordination Services | Orchestration Services | Management Services | Medium | | Management Services | Coordination Services | Uber Management Services | High | | Uber Management Services | Management Services | Aggregation, Views or Exposer Components | Very High |