Skip to content

Commit

Permalink
Merge pull request #54 from marcominerva/develop
Browse files Browse the repository at this point in the history
Add Validation libraries
  • Loading branch information
marcominerva authored Sep 23, 2024
2 parents dee3116 + fc9d0e3 commit 673c832
Show file tree
Hide file tree
Showing 23 changed files with 744 additions and 7 deletions.
53 changes: 53 additions & 0 deletions .github/workflows/publish_fluentvalidation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Publish FluentValidation Helpers on NuGet

on:
push:
branches: [ master ]
paths: [ 'src/MinimalHelpers.Validation.Abstractions/**', 'src/MinimalHelpers.FluentValidation/**' ]
workflow_dispatch:

env:
NET_VERSION: '8.x'
PROJECT_NAME: src/MinimalHelpers.FluentValidation
PROJECT_FILE: MinimalHelpers.FluentValidation.csproj
TAG_NAME: routing
RELEASE_NAME: MinimalHelpers.FluentValidation

jobs:
publish:
name: Publish on NuGet
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0 # avoid shallow clone so nbgv can do its work.

- name: Setup .NET SDK ${{ env.NET_VERSION }}
uses: actions/setup-dotnet@v3
with:
dotnet-version: ${{ env.NET_VERSION }}
dotnet-quality: 'ga'

- name: Nerdbank.GitVersioning
uses: dotnet/[email protected]
id: nbgv
with:
path: ${{ env.PROJECT_NAME }}

- name: Package
run: dotnet pack -c Release -o . '${{ env.PROJECT_NAME }}/${{ env.PROJECT_FILE }}'

- name: Publish on NuGet
run: dotnet nuget push *.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json

- name: Create release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
with:
tag_name: ${{ env.TAG_NAME }}_v${{ steps.nbgv.outputs.NuGetPackageVersion }}
release_name: ${{ env.RELEASE_NAME }} ${{ steps.nbgv.outputs.NuGetPackageVersion }}
draft: false
prerelease: false
53 changes: 53 additions & 0 deletions .github/workflows/publish_validation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Publish Validation Helpers on NuGet

on:
push:
branches: [ master ]
paths: [ 'src/MinimalHelpers.Validation.Abstractions/**', 'src/MinimalHelpers.Validation/**' ]
workflow_dispatch:

env:
NET_VERSION: '8.x'
PROJECT_NAME: src/MinimalHelpers.Validation
PROJECT_FILE: MinimalHelpers.Validation.csproj
TAG_NAME: routing
RELEASE_NAME: MinimalHelpers.Validation

jobs:
publish:
name: Publish on NuGet
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0 # avoid shallow clone so nbgv can do its work.

- name: Setup .NET SDK ${{ env.NET_VERSION }}
uses: actions/setup-dotnet@v3
with:
dotnet-version: ${{ env.NET_VERSION }}
dotnet-quality: 'ga'

- name: Nerdbank.GitVersioning
uses: dotnet/[email protected]
id: nbgv
with:
path: ${{ env.PROJECT_NAME }}

- name: Package
run: dotnet pack -c Release -o . '${{ env.PROJECT_NAME }}/${{ env.PROJECT_FILE }}'

- name: Publish on NuGet
run: dotnet nuget push *.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json

- name: Create release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
with:
tag_name: ${{ env.TAG_NAME }}_v${{ steps.nbgv.outputs.NuGetPackageVersion }}
release_name: ${{ env.RELEASE_NAME }} ${{ steps.nbgv.outputs.NuGetPackageVersion }}
draft: false
prerelease: false
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -351,3 +351,6 @@ MigrationBackup/

/src/MinimalHelpers.Routing/MinimalHelpers.Routing.xml
/src/MinimalHelpers.OpenApi/MinimalHelpers.OpenApi.xml
/src/MinimalHelpers.Validation.Abstractions/MinimalHelpers.Validation.Abstractions.xml
/src/MinimalHelpers.Validation/MinimalHelpers.Validation.xml
/src/MinimalHelpers.FluentValidation/MinimalHelpers.FluentValidation.xml
23 changes: 23 additions & 0 deletions MinimalHelpers.sln
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OpenApi", "OpenApi", "{7B08
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Routing", "Routing", "{6BD388AA-32D1-4B35-B871-02C57F51F0CB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MinimalHelpers.Validation.Abstractions", "src\MinimalHelpers.Validation.Abstractions\MinimalHelpers.Validation.Abstractions.csproj", "{92B52175-C4E1-4478-8B43-DD4E37631B68}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Validation", "Validation", "{773739A3-FEB1-41C5-8CBF-29EBC9A4EC26}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MinimalHelpers.Validation", "src\MinimalHelpers.Validation\MinimalHelpers.Validation.csproj", "{9B903D30-DC15-465D-B7F5-6281EECBFB52}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MinimalHelpers.FluentValidation", "src\MinimalHelpers.FluentValidation\MinimalHelpers.FluentValidation.csproj", "{299262BC-6994-4DB5-8975-113B747CAE48}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -45,6 +53,18 @@ Global
{D3D6F2F5-FC23-4FAA-997E-935CD5CFF38D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D3D6F2F5-FC23-4FAA-997E-935CD5CFF38D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D3D6F2F5-FC23-4FAA-997E-935CD5CFF38D}.Release|Any CPU.Build.0 = Release|Any CPU
{92B52175-C4E1-4478-8B43-DD4E37631B68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{92B52175-C4E1-4478-8B43-DD4E37631B68}.Debug|Any CPU.Build.0 = Debug|Any CPU
{92B52175-C4E1-4478-8B43-DD4E37631B68}.Release|Any CPU.ActiveCfg = Release|Any CPU
{92B52175-C4E1-4478-8B43-DD4E37631B68}.Release|Any CPU.Build.0 = Release|Any CPU
{9B903D30-DC15-465D-B7F5-6281EECBFB52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9B903D30-DC15-465D-B7F5-6281EECBFB52}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9B903D30-DC15-465D-B7F5-6281EECBFB52}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9B903D30-DC15-465D-B7F5-6281EECBFB52}.Release|Any CPU.Build.0 = Release|Any CPU
{299262BC-6994-4DB5-8975-113B747CAE48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{299262BC-6994-4DB5-8975-113B747CAE48}.Debug|Any CPU.Build.0 = Debug|Any CPU
{299262BC-6994-4DB5-8975-113B747CAE48}.Release|Any CPU.ActiveCfg = Release|Any CPU
{299262BC-6994-4DB5-8975-113B747CAE48}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -54,6 +74,9 @@ Global
{6220D0C3-50AD-4C4B-9EDC-49B9F5AF0668} = {6BD388AA-32D1-4B35-B871-02C57F51F0CB}
{A4DDF9B4-7D23-4FF9-9259-89C7179A7DC8} = {7B088E71-866F-43F2-8E73-9B444DAD3FD7}
{D3D6F2F5-FC23-4FAA-997E-935CD5CFF38D} = {6BD388AA-32D1-4B35-B871-02C57F51F0CB}
{92B52175-C4E1-4478-8B43-DD4E37631B68} = {773739A3-FEB1-41C5-8CBF-29EBC9A4EC26}
{9B903D30-DC15-465D-B7F5-6281EECBFB52} = {773739A3-FEB1-41C5-8CBF-29EBC9A4EC26}
{299262BC-6994-4DB5-8975-113B747CAE48} = {773739A3-FEB1-41C5-8CBF-29EBC9A4EC26}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {38B57A92-6EA7-4B79-8400-F4BE44873C46}
Expand Down
183 changes: 183 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,189 @@ endpoints.MapPost("login", LoginAsync)
});
```

## MinimalHelpers.Validation

[![Nuget](https://img.shields.io/nuget/v/MinimalHelpers.Validation)](https://www.nuget.org/packages/MinimalHelpers.Validation)
[![Nuget](https://img.shields.io/nuget/dt/MinimalHelpers.Validation)](https://www.nuget.org/packages/MinimalHelpers.Validation)
A library that provides an Endpoint filter for Minimal API projects to perform validation with Data Annotations, using the <a href="https://github.com/DamianEdwards/MiniValidation">MiniValidation</a> library.

### Installation

The library is available on [NuGet](https://www.nuget.org/packages/MinimalHelpers.Validation). Just search for *MinimalHelpers.Validation* in the **Package Manager GUI** or run the following command in the **.NET CLI**:
```shell
dotnet add package MinimalHelpers.Validation
```

### Usage

Decorates a class with attributes to define the validation rules:

```csharp
using System.ComponentModel.DataAnnotations;

public class Person
{
[Required]
[MaxLength(20)]
public string? FirstName { get; set; }

[Required]
[MaxLength(20)]
public string? LastName { get; set; }

[MaxLength(50)]
public string? City { get; set; }
}
```

Add the `WithValidation<T>()` extension method to enable the validation filter:

```csharp
using MinimalHelpers.Validation;

app.MapPost("/api/people", (Person person) =>
{
// ...
})
.WithValidation<Person>();
```

If the validation fails, the response will be a `400 Bad Request` with a `ValidationProblemDetails` object containing the validation errors, for example:

```json
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred",
"status": 400,
"instance": "/api/people",
"traceId": "00-009c0162ba678cae2ee391815dbbb59d-0a3a5b0c16d053e6-00",
"errors": {
"FirstName": [
"The field FirstName must be a string or array type with a maximum length of '20'."
],
"LastName": [
"The LastName field is required."
]
}
}
```

If you want to customize validation, you can use the `ConfigureValidation` extension method:

```csharp
using MinimalHelpers.Validation;

builder.Services.ConfigureValidation(options =>
{
// If you want to get errors as a list instead of a dictionary.
options.ErrorResponseFormat = ErrorResponseFormat.List;

// The default is "One or more validation errors occurred"
options.ValidationErrorTitleMessageFactory =
(context, errors) => $"There was {errors.Values.Sum(v => v.Length)} error(s)";
});
```

You can use the `ValidationErrorTitleMessageFactory`, for example, if you want to localized the `title` property of the response using a RESX file.

## MinimalHelpers.FluentValidation

[![Nuget](https://img.shields.io/nuget/v/MinimalHelpers.FluentValidation)](https://www.nuget.org/packages/MinimalHelpers.FluentValidation)
[![Nuget](https://img.shields.io/nuget/dt/MinimalHelpers.FluentValidation)](https://www.nuget.org/packages/MinimalHelpers.FluentValidation)
A library that provides an Endpoint filter for Minimal API projects to perform validation using <a href="https://fluentvalidation.net">FluentValidation</a>.

### Installation

The library is available on [NuGet](https://www.nuget.org/packages/MinimalHelpers.FluentValidation). Just search for *MinimalHelpers.FluentValidation* in the **Package Manager GUI** or run the following command in the **.NET CLI**:
```shell
dotnet add package MinimalHelpers.FluentValidation
```

### Usage

Create a class that extends AbstractValidator<T> and define the validation rules:

```csharp
using FluentValidation;

public record class Product(string Name, string Description, double UnitPrice);

public class ProductValidator : AbstractValidator<Product>
{
public ProductValidator()
{
RuleFor(p => p.Name).NotEmpty().MaximumLength(50).EmailAddress();
RuleFor(p => p.Description).MaximumLength(500);
RuleFor(p => p.UnitPrice).GreaterThan(0);
}
}
```

Register validators in the Service Collection:

```csharp
using FluentValidation;

// Assuming the validators are in the same assembly as the Program class
builder.Services.AddValidatorsFromAssemblyContaining<Program>();

```

Add the `WithValidation<T>()` extension method to enable the validation filter:

```csharp
using MinimalHelpers.FluentValidation;

app.MapPost("/api/products", (Product product) =>
{
// ...
})
.WithValidation<Product>();
```

If the validation fails, the response will be a `400 Bad Request` with a `ValidationProblemDetails` object containing the validation errors, for example:

```json
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred",
"status": 400,
"instance": "/api/products",
"traceId": "00-f4ced0ae470424dd04cbcebe5f232dc5-bbdcc59f310ebfb8-00",
"errors": {
"Name": [
"'Name' cannot be empty."
],
"UnitPrice": [
"'Unit Price' must be grater than '0'."
]
}
}
```

If you want to customize validation, you can use the `ConfigureValidation` extension method:

```csharp
using MinimalHelpers.Validation;

builder.Services.ConfigureValidation(options =>
{
// If you want to get errors as a list instead of a dictionary.
options.ErrorResponseFormat = ErrorResponseFormat.List;

// The default is "One or more validation errors occurred"
options.ValidationErrorTitleMessageFactory =
(context, errors) => $"There was {errors.Values.Sum(v => v.Length)} error(s)";
});
```

You can use the `ValidationErrorTitleMessageFactory`, for example, if you want to localized the `title` property of the response using a RESX file.


**Contribute**

The project is constantly evolving. Contributions are welcome. Feel free to file issues and pull requests on the repo and we'll address them as we can.
31 changes: 27 additions & 4 deletions samples/MinimalSample/Handlers/PeopleHandler.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
namespace MinimalSample.Handlers;
using System.ComponentModel.DataAnnotations;
using MinimalHelpers.Validation;

namespace MinimalSample.Handlers;

public class PeopleHandler : IEndpointRouteHandlerBuilder
{
public static void MapEndpoints(IEndpointRouteBuilder endpoints)
{
endpoints.MapGet("/api/people", GetList);

endpoints.MapGet("/api/people/{id:guid}", Get);
endpoints.MapPost("/api/people", Insert);
endpoints.MapPut("/api/people/{id:guid}", Update);

endpoints.MapPost("/api/people", Insert)
// MinimalHelpers.Validation package performs validation with Data Annotations.
.WithValidation<Person>();

endpoints.MapPut("/api/people/{id:guid}", Update)
// MinimalHelpers.Validation package performs validation with Data Annotations.
.WithValidation<Person>();

endpoints.MapDelete("/api/people/{id:guid}", Delete);
}

Expand All @@ -22,4 +33,16 @@ public static void MapEndpoints(IEndpointRouteBuilder endpoints)
private static IResult Delete(Guid id) => TypedResults.NoContent();
}

public record class Person(string FirstName, string LastName);
public class Person
{
[Required]
[MaxLength(20)]
public string? FirstName { get; set; }

[Required]
[MaxLength(20)]
public string? LastName { get; set; }

[MaxLength(50)]
public string? City { get; set; }
}
Loading

0 comments on commit 673c832

Please sign in to comment.