Skip to content

Commit

Permalink
Improved documentation stack and added additional Roslyn rules for co…
Browse files Browse the repository at this point in the history
…mmon mistakes. Closes #40
  • Loading branch information
jezzsantos committed Jul 8, 2024
1 parent 18f27bb commit 22a0083
Show file tree
Hide file tree
Showing 27 changed files with 1,452 additions and 240 deletions.
18 changes: 14 additions & 4 deletions docs/how-to-guides/020-api-endpoint.md
Original file line number Diff line number Diff line change
Expand Up @@ -787,13 +787,17 @@ public class RegisterCarRequestValidatorSpec

Lastly, we strongly recommend writing at lest one integration test per service operation that you define.

You NEED at least one integration test to cover and verify that all your layers ae wired up correctly, and that they are defined correctly.
You NEED at least one integration test to cover and verify that all your layers are wired up correctly and that they are working together.

Then we recommend, that you write a handful of integration tests, to make sure that known common use cases you designed for, work as you expected. This is the best way to build confidence that your API works end to end. Without which you are manually testing by hand.
Then we recommend that you write a handful of integration tests to make sure that the known common use cases you designed for work as you expected.

This is the best approach to build your confidence that your API works end to end.

It is more effort for you, until you hit a production issue, and have to debug something simple that could have easily been avoided with an integration test.

> Once you get used to this process, you will find that you no longer need to run HTTP testing tools like Postman, and can just rely on running integration tests. You will also spend significantly less time running the code locally in the debugger and identifying issues. You will literally save hours of work in your day this way.

The number of additional integration tests, that you will write here will depend on the complexity of each API call.
The number of additional integration tests that you will write here will depend on the complexity of each API call.

> Note: It is these integration tests that you will start writing more of to cover these cases when you encounter a bug in production. The process goes like this: You learn about the bug, you write a new integration test for the API that is the source of the bug, and you use production data if necessary to reproduce the bug. Then you debug and identify the root cause, and then you replace the production data with regular test data to reproduce the error. Now you are left with an extra integration test to verify that you actually fixed the issue. And it stays there for years.

Expand All @@ -807,7 +811,13 @@ The last benefit here, is that when you finish the implementation of the Applica

Your integration tests should be written into the `Infrastructure.IntegrationTests` project in your subdomain. For example, the `CarsInfrastructure.IntegrationTests` project for the `CarsApi`.

These tests follow a common pattern also, that runs your entire codebase in a testing host (ASPNET `TestServer`) and gives you access to a typed testing client for calling your API easily.
Create that project in the `Tests` solution folder, using the `SaaStack Integration Test` project template.

Then rename the included tests class to reflect the name of your API class.

For example, `CarsApiSpec.cs`

These tests follow a common pattern that runs your entire codebase in a testing host (ASPNET `TestServer`) and gives you access to a typed testing client for calling your API very easily.

For example,

Expand Down
6 changes: 5 additions & 1 deletion docs/how-to-guides/040-domain-layer.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,4 +196,8 @@ public class CarRootSpec

... other tests
}
```
```

### Next Steps

See
13 changes: 13 additions & 0 deletions docs/how-to-guides/110-application-repository.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Application Repository

## Why?

Description of a specific problem that you might have

## What is the mechanism?

Explanation of where we do this and how

## Where to start?

Steps to do it
13 changes: 13 additions & 0 deletions docs/how-to-guides/120-aggregates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Aggregate Roots

## Why?

Description of a specific problem that you might have

## What is the mechanism?

Explanation of where we do this and how

## Where to start?

Steps to do it
13 changes: 13 additions & 0 deletions docs/how-to-guides/130-child-entities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Entities

## Why?

Description of a specific problem that you might have

## What is the mechanism?

Explanation of where we do this and how

## Where to start?

Steps to do it
13 changes: 13 additions & 0 deletions docs/how-to-guides/140-valueobjects.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# ValueObjects

## Why?

Description of a specific problem that you might have

## What is the mechanism?

Explanation of where we do this and how

## Where to start?

Steps to do it
13 changes: 13 additions & 0 deletions docs/how-to-guides/150-application-services.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Application Services

## Why?

Description of a specific problem that you might have

## What is the mechanism?

Explanation of where we do this and how

## Where to start?

Steps to do it
10 changes: 5 additions & 5 deletions docs/how-to-guides/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
* [Add a new API](020-api-endpoint.md)
* [Define your application](030-application-layer.md)
* [Define your domain](040-domain-layer.md)
* (coming soon) Create a new application repository
* (coming soon) Define a use case in a Root Aggregate
* (coming soon) Create a child Entity of a Root Aggregate
* (coming soon) Create a ValueObject
* (coming soon) Make a cross-domain call
* (coming soon) [Create a new application Repository](110-application-repository.md)
* (coming soon) [Define a use case in an Aggregate Root](120-aggregates.md)
* (coming soon) [Create a child Entity of an Aggregate Root](130-child-entities.md)
* (coming soon) [Create a ValueObject](140-valueobjects.md)
* (coming soon) [Make a cross-domain call](150-application-services.md)
* [Handle Domain Events](090-handle-domain-events.md) raised by other subdomains
* [Build an adapter](100-build-adapter-third-party.md) to implement an integration to a 3rd party service (over HTTP)
* (coming soon) Build a new technology adapter (i.e. database persistence)
Expand Down
34 changes: 34 additions & 0 deletions src/Common.UnitTests/Extensions/CollectionExtensionsSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,38 @@ public void WhenContainsIgnoreCaseAndMatches_ThenReturnsTrue()

result.Should().BeTrue();
}

[Fact]
public void WhenNotContainsAndEmpty_ThenReturnsTrue()
{
var result = new List<string>().NotContains(item => item == "avalue");

result.Should().BeTrue();
}

[Fact]
public void WhenNotContainsAndNotMatches_ThenReturnsTrue()
{
var result = new List<string>
{
"avalue1",
"avalue2",
"avalue3"
}.NotContains(item => item == "avalue9");

result.Should().BeTrue();
}

[Fact]
public void WhenNotContainsAndMatches_ThenReturnsFalse()
{
var result = new List<string>
{
"avalue1",
"avalue2",
"avalue3"
}.NotContains(item => item == "avalue2");

result.Should().BeFalse();
}
}
22 changes: 15 additions & 7 deletions src/Common/Extensions/CollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,31 @@ public static TResult First<TResult>(this IReadOnlyList<TResult> list)
/// <summary>
/// Joins all values separated by the <see cref="separator" />
/// </summary>
public static string Join<T>(this IEnumerable<T> values, string separator)
public static string Join<T>(this IEnumerable<T> collection, string separator)
{
var stringBuilder = new StringBuilder();
foreach (var value in values)
foreach (var item in collection)
{
if (stringBuilder.Length > 0)
{
stringBuilder.Append(separator);
}

stringBuilder.Append(value);
stringBuilder.Append(item);
}

return stringBuilder.ToString();
}
#endif
#if COMMON_PROJECT || ANALYZERS_NONPLATFORM
/// <summary>
/// Whether the specified collection contains an item that matched the specified <see cref="predicate" />
/// </summary>
public static bool NotContains<T>(this IEnumerable<T> collection, Predicate<T> predicate)
{
return !collection.Any(item => predicate(item));
}

/// <summary>
/// Whether the <see cref="target" /> string does not exist in the <see cref="collection" />
/// </summary>
Expand Down Expand Up @@ -95,17 +103,17 @@ public static bool HasNone<T>(this IEnumerable<T> collection)
/// <summary>
/// Returns a string value for all the items in the list, separated by the specified <see cref="orKeyword" />
/// </summary>
public static string JoinAsOredChoices(this IEnumerable<string> list, string orKeyword = ",")
public static string JoinAsOredChoices(this IEnumerable<string> collection, string orKeyword = ",")
{
return list.Join($"{orKeyword} ");
return collection.Join($"{orKeyword} ");
}

/// <summary>
/// Returns the last item in the collection
/// </summary>
public static TResult Last<TResult>(this IReadOnlyList<TResult> list)
public static TResult Last<TResult>(this IReadOnlyList<TResult> collection)
{
return list[^1];
return collection[^1];
}
#endif
}
10 changes: 6 additions & 4 deletions src/Common/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Text;
using System.Text.RegularExpressions;
#endif

#if GENERATORS_COMMON_PROJECT
Expand All @@ -39,7 +40,7 @@ public enum JsonCasing
Camel
}
#endif
#if COMMON_PROJECT
#if COMMON_PROJECT || ANALYZERS_NONPLATFORM
private static readonly TimeSpan DefaultRegexTimeout = TimeSpan.FromSeconds(10);
#endif
#if COMMON_PROJECT || GENERATORS_WEB_API_PROJECT || ANALYZERS_NONPLATFORM
Expand Down Expand Up @@ -160,7 +161,7 @@ public static bool HasValue([NotNullWhen(true)] this string? value)
return !string.IsNullOrEmpty(value) && !string.IsNullOrWhiteSpace(value);
}
#endif
#if COMMON_PROJECT
#if COMMON_PROJECT || ANALYZERS_NONPLATFORM
/// <summary>
/// Whether the <see cref="value" /> matches the <see cref="pattern" />
/// Avoid potential DOS attacks where the regex may timeout if too complex
Expand All @@ -175,7 +176,8 @@ public static bool IsMatchWith(this string value, [RegexPattern] string pattern,
var timeoutSafe = timeout ?? DefaultRegexTimeout;
return Regex.IsMatch(value, pattern, RegexOptions.None, timeoutSafe);
}

#endif
#if COMMON_PROJECT
/// <summary>
/// Whether the <see cref="other" /> is not the same as the value (case-insensitive)
/// </summary>
Expand Down Expand Up @@ -439,7 +441,7 @@ public static long ToLongOrDefault(this string? value, long defaultValue)
#endif
#if COMMON_PROJECT || GENERATORS_COMMON_PROJECT
/// <summary>
/// Returns the specified <see cref="value" /> in snake_case. i.e. lower case with underscores for upper cased
/// Returns the specified <see cref="value" /> in snake_case. i.e. lower case with underscores for uppercased
/// letters
/// </summary>
public static string ToSnakeCase(this string value)
Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
<!-- Analyzers -->
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn),1573,1574,1591,1712,1723</NoWarn>
<NoWarn>$(NoWarn),1573,1574,1591,1712,1723,SAASDDD037</NoWarn>
</PropertyGroup>

<!-- Runs the analyzers (in memory) on build -->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
namespace Domain.Interfaces.ValueObjects;

/// <summary>
/// Marks the method as being ignored by any immutability checks (performed by the Roslyn analyzers)
/// Skips immutability checks performed on all methods of valueobjects to ensure immutability.
/// Used for methods that have other utility, that definitely do not mutate the state of this instance
/// </summary>
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
public class SkipImmutabilityCheckAttribute : Attribute;
1 change: 1 addition & 0 deletions src/SaaStack.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -1801,6 +1801,7 @@ public void When$condition$_Then$outcome$()
<s:Boolean x:Key="/Default/UserDictionary/Words/=Underlapped/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=unknowntype/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=upserted/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=valueobjects/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=_0020_0020_0020_0020_0020_0020/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=_0020_0022areferen/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=_0022anunknowntyp/@EntryIndexedValue">True</s:Boolean>
Expand Down
1 change: 1 addition & 0 deletions src/Tools.Analyzers.Common/AnalyzerConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ public static class AnalyzerConstants
{
public const string RequestTypeSuffix = "Request";
public const string ResourceTypesNamespace = "Application.Resources.Shared";
public const string DomainEventTypesNamespace = "Domain.Events.Shared";
public const string ResponseTypeSuffix = "Response";
public const string ServiceOperationTypesNamespace = "Infrastructure.Web.Api.Operations.Shared";
public static readonly string[] PlatformNamespaces =
Expand Down
Loading

0 comments on commit 22a0083

Please sign in to comment.