Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ jobs:
uses: gpreviatti/github-actions-templates/.github/workflows/dotnet-pack.yml@v1
with:
dotnet_version: "${{ vars.PROJECT_DOTNET_VERSION }}"
package_version: "10.15.0"
package_version: "10.16.0"
secrets:
nuget_api_key: ${{ secrets.NUGET_API_KEY }}

3 changes: 2 additions & 1 deletion GPreviatti.Template.Hexagonal.Solution.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
<RepositoryUrl>git://github.com/gpreviatti/hexagonal-solution-template</RepositoryUrl>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageReleaseNotes>
- Remove unnecessary library from Bff project
- Add full text search use case on Full template
- Add full text search request and response on Contracts template
</PackageReleaseNotes>
</PropertyGroup>
<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion templates/Contracts/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ tests/UnitTests/

### C# Contracts
- All contracts are `sealed record` — immutable, serialization-safe, value-equal by default
- All requests **must** extend `BaseRequest(Guid CorrelationId)` or `BasePaginatedRequest`
- All requests **must** extend `BaseRequest(Guid CorrelationId)` or `BasePaginatedRequest` or `BaseFullTextSearchPaginatedRequest`
- All responses **must** extend `BaseResponse(bool IsSuccess, string Message)`
- Use file-scoped namespaces (`namespace Contracts.[Domain];`)
- Nullable reference types are enabled — annotate all nullable properties with `?`
Expand Down
34 changes: 31 additions & 3 deletions templates/Contracts/src/Contracts/Common/BaseRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
/// Base request structure
/// </summary>
/// <param name="CorrelationId">The unique identifier for correlating requests</param>
public record BaseRequest(Guid CorrelationId);
/// <param name="User">The user making the request</param>
/// <param name="TimezoneId">The timezone identifier for the request</param>
public record BaseRequest(Guid CorrelationId, string User = "", string TimezoneId = "");

/// <summary>
/// Base paginated request structure
Expand All @@ -15,11 +17,37 @@ public record BaseRequest(Guid CorrelationId);
/// <param name="SortBy">The field to sort by</param>
/// <param name="SortDescending">Indicates whether the sorting is in descending order</param>
/// <param name="SearchByValues">A dictionary of search criteria</param>
/// <param name="User">The user making the request</param>
/// <param name="TimezoneId">The timezone identifier for the request</param>
public record BasePaginatedRequest(
Guid CorrelationId,
int Page = 1,
int PageSize = 10,
string? SortBy = null,
bool SortDescending = false,
Dictionary<string, string>? SearchByValues = null
) : BaseRequest(CorrelationId);
Dictionary<string, string>? SearchByValues = null,
string User = "",
string TimezoneId = ""
) : BaseRequest(CorrelationId, User, TimezoneId);

/// <summary>
/// Represents a request for paginated data with full-text search capabilities.
/// </summary>
/// <param name="CorrelationId">The unique identifier for correlating requests</param>
/// <param name="Page">The page number to retrieve</param>
/// <param name="PageSize">The number of items per page</param>
/// <param name="SortBy">The field to sort by</param>
/// <param name="SortDescending">Indicates whether the sorting is in descending order</param>
/// <param name="SearchValue">The value to search for within the full-text index e.g. "search term"</param>
/// <param name="User">The user making the request</param>
/// <param name="TimezoneId">The timezone identifier for the request</param>
public record BaseFullTextSearchPaginatedRequest(
Guid CorrelationId,
int Page = 1,
int PageSize = 10,
string? SortBy = null,
bool SortDescending = false,
string SearchValue = "",
string User = "",
string TimezoneId = ""
) : BaseRequest(CorrelationId, User, TimezoneId);
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ namespace Contracts.Orders;
/// <param name="CorrelationId">The unique identifier for the request</param>
/// <param name="Description">A description of the order</param>
/// <param name="Items">The items included in the order</param>
/// <param name="CreatedBy">The user creating the order</param>
/// <param name="TimezoneId">The timezone identifier for the request</param>
public sealed record CreateOrderRequest(
Guid CorrelationId,
string Description,
CreateOrderItemRequest[] Items
) : BaseRequest(CorrelationId);
CreateOrderItemRequest[] Items,
string CreatedBy = "",
string TimezoneId = ""
) : BaseRequest(CorrelationId, CreatedBy, TimezoneId);

/// <summary>
/// An item included in the order
Expand Down
85 changes: 84 additions & 1 deletion templates/Contracts/tests/UnitTests/Common/BaseRequestTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,24 @@ public void GivenABaseRequestWhenInstantiatedThenShouldAssignCorrelationId()
Assert.Equal(correlationId, request.CorrelationId);
}

[Fact(DisplayName = nameof(GivenABaseRequestWhenInstantiatedThenShouldAssignDefaultUserAndTimezone))]
public void GivenABaseRequestWhenInstantiatedThenShouldAssignDefaultUserAndTimezone()
{
var request = new BaseRequest(Guid.NewGuid());

Assert.Equal(string.Empty, request.User);
Assert.Equal(string.Empty, request.TimezoneId);
}

[Fact(DisplayName = nameof(GivenABaseRequestWhenInstantiatedWithCustomUserAndTimezoneThenShouldAssignValues))]
public void GivenABaseRequestWhenInstantiatedWithCustomUserAndTimezoneThenShouldAssignValues()
{
var request = new BaseRequest(Guid.NewGuid(), User: "alice", TimezoneId: "America/New_York");

Assert.Equal("alice", request.User);
Assert.Equal("America/New_York", request.TimezoneId);
}

[Fact(DisplayName = nameof(GivenABasePaginatedRequestWhenUsingDefaultValuesThenShouldAssignExpectedDefaults))]
public void GivenABasePaginatedRequestWhenUsingDefaultValuesThenShouldAssignExpectedDefaults()
{
Expand All @@ -27,6 +45,8 @@ public void GivenABasePaginatedRequestWhenUsingDefaultValuesThenShouldAssignExpe
Assert.Null(request.SortBy);
Assert.False(request.SortDescending);
Assert.Null(request.SearchByValues);
Assert.Equal(string.Empty, request.User);
Assert.Equal(string.Empty, request.TimezoneId);
}

[Fact(DisplayName = nameof(GivenABasePaginatedRequestWhenInstantiatedThenShouldAssignCustomValues))]
Expand All @@ -44,7 +64,9 @@ public void GivenABasePaginatedRequestWhenInstantiatedThenShouldAssignCustomValu
PageSize: 25,
SortBy: "CreatedAt",
SortDescending: true,
SearchByValues: searchByValues
SearchByValues: searchByValues,
User: "bob",
TimezoneId: "Europe/London"
);

Assert.Equal(correlationId, request.CorrelationId);
Expand All @@ -53,5 +75,66 @@ public void GivenABasePaginatedRequestWhenInstantiatedThenShouldAssignCustomValu
Assert.Equal("CreatedAt", request.SortBy);
Assert.True(request.SortDescending);
Assert.Equal(searchByValues, request.SearchByValues);
Assert.Equal("bob", request.User);
Assert.Equal("Europe/London", request.TimezoneId);
}

[Fact(DisplayName = nameof(GivenABaseFullTextSearchPaginatedRequestWhenUsingDefaultValuesThenShouldAssignExpectedDefaults))]
public void GivenABaseFullTextSearchPaginatedRequestWhenUsingDefaultValuesThenShouldAssignExpectedDefaults()
{
var correlationId = Guid.NewGuid();

var request = new BaseFullTextSearchPaginatedRequest(correlationId);

Assert.Equal(correlationId, request.CorrelationId);
Assert.Equal(1, request.Page);
Assert.Equal(10, request.PageSize);
Assert.Null(request.SortBy);
Assert.False(request.SortDescending);
Assert.Equal(string.Empty, request.SearchValue);
Assert.Equal(string.Empty, request.User);
Assert.Equal(string.Empty, request.TimezoneId);
}

[Fact(DisplayName = nameof(GivenABaseFullTextSearchPaginatedRequestWhenInstantiatedThenShouldAssignCustomValues))]
public void GivenABaseFullTextSearchPaginatedRequestWhenInstantiatedThenShouldAssignCustomValues()
{
var correlationId = Guid.NewGuid();

var request = new BaseFullTextSearchPaginatedRequest(
correlationId,
Page: 3,
PageSize: 20,
SortBy: "Name",
SortDescending: true,
SearchValue: "laptop",
User: "carol",
TimezoneId: "Asia/Tokyo"
);

Assert.Equal(correlationId, request.CorrelationId);
Assert.Equal(3, request.Page);
Assert.Equal(20, request.PageSize);
Assert.Equal("Name", request.SortBy);
Assert.True(request.SortDescending);
Assert.Equal("laptop", request.SearchValue);
Assert.Equal("carol", request.User);
Assert.Equal("Asia/Tokyo", request.TimezoneId);
}

[Fact(DisplayName = nameof(GivenABaseFullTextSearchPaginatedRequestThenShouldInheritFromBaseRequest))]
public void GivenABaseFullTextSearchPaginatedRequestThenShouldInheritFromBaseRequest()
{
var request = new BaseFullTextSearchPaginatedRequest(Guid.NewGuid());

Assert.IsAssignableFrom<BaseRequest>(request);
}

[Fact(DisplayName = nameof(GivenABasePaginatedRequestThenShouldInheritFromBaseRequest))]
public void GivenABasePaginatedRequestThenShouldInheritFromBaseRequest()
{
var request = new BasePaginatedRequest(Guid.NewGuid());

Assert.IsAssignableFrom<BaseRequest>(request);
}
}
59 changes: 59 additions & 0 deletions templates/Contracts/tests/UnitTests/Common/BaseResponseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,25 @@ public void GivenABaseResponseWhenInstantiatedThenShouldAssignSuccessAndMessage(
Assert.Equal("ok", response.Message);
}

[Fact(DisplayName = nameof(GivenABaseResponseWhenInstantiatedWithSuccessOnlyThenMessageShouldBeNull))]
public void GivenABaseResponseWhenInstantiatedWithSuccessOnlyThenMessageShouldBeNull()
{
var response = new BaseResponse(true);

Assert.True(response.Success);
Assert.Null(response.Message);
}

[Fact(DisplayName = nameof(GivenABaseResponseOfTDataWhenInstantiatedWithDefaultConstructorThenShouldHaveDefaultValues))]
public void GivenABaseResponseOfTDataWhenInstantiatedWithDefaultConstructorThenShouldHaveDefaultValues()
{
var response = new BaseResponse<OrderDto>();

Assert.False(response.Success);
Assert.Null(response.Message);
Assert.Null(response.Data);
}

[Fact(DisplayName = nameof(GivenABaseResponseOfTDataWhenInstantiatedThenShouldAssignSuccessMessageAndData))]
public void GivenABaseResponseOfTDataWhenInstantiatedThenShouldAssignSuccessMessageAndData()
{
Expand All @@ -40,6 +59,28 @@ public void GivenABaseResponseOfTDataWhenInstantiatedThenShouldAssignSuccessMess
Assert.Equal(data, response.Data);
}

[Fact(DisplayName = nameof(GivenABaseResponseOfTDataWhenInstantiatedWithNullDataThenDataShouldBeNull))]
public void GivenABaseResponseOfTDataWhenInstantiatedWithNullDataThenDataShouldBeNull()
{
var response = new BaseResponse<OrderDto>(false, null, "not found");

Assert.False(response.Success);
Assert.Equal("not found", response.Message);
Assert.Null(response.Data);
}

[Fact(DisplayName = nameof(GivenABasePaginatedResponseWhenInstantiatedWithDefaultConstructorThenShouldHaveDefaultValues))]
public void GivenABasePaginatedResponseWhenInstantiatedWithDefaultConstructorThenShouldHaveDefaultValues()
{
var response = new BasePaginatedResponse<ItemDto>();

Assert.False(response.Success);
Assert.Null(response.Message);
Assert.Null(response.Data);
Assert.Equal(0, response.TotalPages);
Assert.Equal(0, response.TotalRecords);
}

[Fact(DisplayName = nameof(GivenABasePaginatedResponseWhenInstantiatedThenShouldAssignPaginationAndData))]
public void GivenABasePaginatedResponseWhenInstantiatedThenShouldAssignPaginationAndData()
{
Expand Down Expand Up @@ -68,4 +109,22 @@ public void GivenABasePaginatedResponseWhenInstantiatedThenShouldAssignPaginatio
Assert.Equal(30, response.TotalRecords);
Assert.Equal(data, response.Data);
}

[Fact(DisplayName = nameof(GivenABasePaginatedResponseWhenInstantiatedWithNullDataThenDataShouldBeNull))]
public void GivenABasePaginatedResponseWhenInstantiatedWithNullDataThenDataShouldBeNull()
{
var response = new BasePaginatedResponse<ItemDto>(
success: false,
totalPages: 0,
totalRecords: 0,
data: null,
message: "empty"
);

Assert.False(response.Success);
Assert.Equal("empty", response.Message);
Assert.Equal(0, response.TotalPages);
Assert.Equal(0, response.TotalRecords);
Assert.Null(response.Data);
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Contracts.Common;
using Contracts.Orders;

namespace UnitTests.Orders;
Expand Down Expand Up @@ -30,4 +31,57 @@ public void GivenACreateOrderRequestWhenInstantiatedThenShouldAssignProperties()
Assert.Equal("Order description", request.Description);
Assert.Equal(items, request.Items);
}

[Fact(DisplayName = nameof(GivenACreateOrderRequestWhenInstantiatedWithDefaultOptionalParamsThenShouldAssignEmptyStrings))]
public void GivenACreateOrderRequestWhenInstantiatedWithDefaultOptionalParamsThenShouldAssignEmptyStrings()
{
var request = new CreateOrderRequest(Guid.NewGuid(), "Order description", []);

Assert.Equal(string.Empty, request.CreatedBy);
Assert.Equal(string.Empty, request.TimezoneId);
}

[Fact(DisplayName = nameof(GivenACreateOrderRequestWhenInstantiatedWithCustomCreatedByAndTimezoneThenShouldAssignValues))]
public void GivenACreateOrderRequestWhenInstantiatedWithCustomCreatedByAndTimezoneThenShouldAssignValues()
{
var request = new CreateOrderRequest(
Guid.NewGuid(),
"Order description",
[],
CreatedBy: "alice",
TimezoneId: "America/Sao_Paulo"
);

Assert.Equal("alice", request.CreatedBy);
Assert.Equal("America/Sao_Paulo", request.TimezoneId);
}

[Fact(DisplayName = nameof(GivenACreateOrderRequestThenShouldInheritFromBaseRequest))]
public void GivenACreateOrderRequestThenShouldInheritFromBaseRequest()
{
var request = new CreateOrderRequest(Guid.NewGuid(), "desc", []);

Assert.IsAssignableFrom<BaseRequest>(request);
}

[Fact(DisplayName = nameof(GivenACreateOrderRequestWhenCreatedByIsSetThenUserPropertyShouldMatchCreatedBy))]
public void GivenACreateOrderRequestWhenCreatedByIsSetThenUserPropertyShouldMatchCreatedBy()
{
var request = new CreateOrderRequest(
Guid.NewGuid(),
"desc",
[],
CreatedBy: "bob"
);

Assert.Equal("bob", request.User);
}

[Fact(DisplayName = nameof(GivenACreateOrderRequestWhenInstantiatedWithEmptyItemsThenItemsShouldBeEmpty))]
public void GivenACreateOrderRequestWhenInstantiatedWithEmptyItemsThenItemsShouldBeEmpty()
{
var request = new CreateOrderRequest(Guid.NewGuid(), "desc", []);

Assert.Empty(request.Items);
}
}
36 changes: 36 additions & 0 deletions templates/Contracts/tests/UnitTests/Orders/OrderDtoTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,42 @@ public void GivenAnOrderDtoWhenPropertiesAreAssignedThenShouldExposeExpectedValu
Assert.Equal(items, orderDto.Items);
}

[Fact(DisplayName = nameof(GivenAnOrderDtoWhenItemsIsNullThenShouldExposeNullItems))]
public void GivenAnOrderDtoWhenItemsIsNullThenShouldExposeNullItems()
{
var orderDto = new OrderDto
{
Id = 1,
Description = "Order with no items",
Total = 0m,
Items = null
};

Assert.Null(orderDto.Items);
}

[Fact(DisplayName = nameof(GivenAnOrderDtoWhenMultipleItemsAreAssignedThenShouldExposeAllItems))]
public void GivenAnOrderDtoWhenMultipleItemsAreAssignedThenShouldExposeAllItems()
{
ItemDto[] items =
[
new() { Id = 1, Name = "Item A", Description = "Desc A", Value = 10m },
new() { Id = 2, Name = "Item B", Description = "Desc B", Value = 20m },
new() { Id = 3, Name = "Item C", Description = "Desc C", Value = 30m }
];

var orderDto = new OrderDto
{
Id = 5,
Description = "Multi-item order",
Total = 60m,
Items = items
};

Assert.Equal(3, orderDto.Items!.Count);
Assert.Equal(60m, orderDto.Total);
}

[Fact(DisplayName = nameof(GivenAnItemDtoWhenPropertiesAreAssignedThenShouldExposeExpectedValues))]
public void GivenAnItemDtoWhenPropertiesAreAssignedThenShouldExposeExpectedValues()
{
Expand Down
Loading
Loading