Skip to content

Commit

Permalink
moved to GitHub actions, removed deprecations (#14)
Browse files Browse the repository at this point in the history
* moved to GitHub actions, removed deprecations

* added better Nuget badge and build history, linted README.md
  • Loading branch information
baileydoestech authored Jan 18, 2022
1 parent 4bc20bc commit 0b449a4
Show file tree
Hide file tree
Showing 16 changed files with 624 additions and 863 deletions.
83 changes: 83 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
name: Build
on:
push:
branches:
- master
tags:
- "*"
pull_request:
jobs:
calculate-version:
runs-on: ubuntu-20.04
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout code
uses: actions/checkout@master
with:
fetch-depth: 0
- name: Install GitVersion
uses: gittools/actions/gitversion/[email protected]
with:
versionSpec: "5.8.1"
- name: Run GitVersion
id: gitversion
uses: gittools/actions/gitversion/[email protected]
- name: Version
id: version
run: |
version=${{ steps.gitversion.outputs.nuGetVersionV2 }}
if [ "${{ github.event_name }}" == "pull_request" ]
then
version=${version}-${{ steps.gitversion.outputs.shortSha }}
fi
echo "::set-output name=version::${version}"
build:
runs-on: ${{ matrix.os }}
needs: [calculate-version]
strategy:
matrix:
include:
- os: ubuntu-20.04
nugetPush: false
- os: windows-2019
nugetPush: true
- os: macos-10.15
nugetPush: false
steps:
- name: Checkout code
uses: actions/checkout@master
with:
fetch-depth: 0
submodules: recursive
- name: Setup dotnet SDK
uses: actions/setup-dotnet@v1
with:
dotnet-version: "6.0.101"
- name: Build
run: |
dotnet build -c Release -p:Version=${{ needs.calculate-version.outputs.version }}
shell: bash
- name: Test
run: dotnet test -c Release --no-build
shell: bash
- name: Archive NuGet Packages
uses: actions/upload-artifact@v2
if: ${{ matrix.nugetPush }}
with:
name: packages
path: |
**/*.nupkg
**/*.snupkg
retention-days: 1
nuget-push:
runs-on: ubuntu-20.04
needs: [build]
if: github.event_name != 'pull_request'
steps:
- name: Download NuGet Packages
uses: actions/download-artifact@v2
with:
name: packages
- name: NuGet Push
run: dotnet nuget push **/*.nupkg -s https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }}
21 changes: 0 additions & 21 deletions .travis.yml

This file was deleted.

4 changes: 2 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
<PropertyGroup>
<Authors>Winton</Authors>
<Company>Winton</Company>
<Copyright>Copyright 2020 Winton</Copyright>
<Copyright>Copyright 2022 Winton</Copyright>
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)Rules.ruleset</CodeAnalysisRuleSet>
<LangVersion>8.0</LangVersion>
<LangVersion>10.0</LangVersion>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
</PropertyGroup>
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright 2018 Winton
Copyright 2022 Winton

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
41 changes: 21 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
# Winton.DomainModelling.AspNetCore

[![Appveyor](https://ci.appveyor.com/api/projects/status/k94y5or6toq2un7d?svg=true)](https://ci.appveyor.com/project/wintoncode/winton-domainmodelling-aspnetcore/branch/master)
[![Travis CI](https://travis-ci.com/wintoncode/Winton.DomainModelling.AspNetCore.svg?branch=master)](https://travis-ci.com/wintoncode/Winton.DomainModelling.AspNetCore)
[![NuGet version](https://img.shields.io/nuget/v/Winton.DomainModelling.AspNetCore.svg)](https://www.nuget.org/packages/Winton.DomainModelling.AspNetCore)
[![NuGet version](https://img.shields.io/nuget/vpre/Winton.DomainModelling.AspNetCore.svg)](https://www.nuget.org/packages/Winton.DomainModelling.AspNetCore)

Conventions useful for creating an ASP.NET Core based REST API on top of a domain model. Specifically, it provides extension methods which convert from domain model types, as defined in [`Winton.DomainModelling.Abstractions`](https://github.com/wintoncode/Winton.DomainModelling.Abstractions) to ASP.NET Core types.

[![NuGet Badge](https://buildstats.info/nuget/Winton.DomainModelling.AspNetCore)](https://www.nuget.org/packages/Winton.DomainModelling.AspNetCore/)
[![Build history](https://buildstats.info/github/chart/wintoncode/Winton.DomainModelling.AspNetCore?branch=master)](https://github.com/wintoncode/Winton.DomainModelling.AspNetCore/actions)

## `Result` Extensions

`Result<TData>` is a type defined in the `Winton.DomainModelling.Abstractions` package.
`Result<TData>` is a type defined in the `Winton.DomainModelling.Abstractions` package.
It is a type that is intended to be returned from domain operations.
It allows operations to indicate both successes and failures to the client.
In this case the client is an ASP.NET Core Controller.
In a Controller, however, we need to return an `IActionResult` rather than a `Result<TData>`. We have two cases to consider:

* If the `Result<TData>` was a success then we want to return a 2xx response from the API containing the data in the body.
* If the `Result<TData>` was a failure then we want to return a 4xx response from the API containing [problem details](https://tools.ietf.org/html/rfc7807) in the body.

This library provides a `ToActionResult` extension method for `Result<TData>` which matches on the result and converts it to an appropriate `IActionResult`.
There are various overloads to provide flexibility.
There are various overloads to provide flexibility.

It is expected that this will be used within an [`ApiController`](https://docs.microsoft.com/en-us/aspnet/core/web-api/?view=aspnetcore-2.2#annotation-with-apicontroller-attribute) so that ASP.NET Core will apply its REST API conventions to the `IActionResult`.

### Successful Result Mappings
Expand All @@ -30,7 +30,7 @@ The following default mappings happen when the `Result` is a `Success`.
| `Success<TData>` | `ActionResult<TData>` | 200 Ok |
| `Success<Unit>` | `NoContentResult` | 204 NoContent |

The defaults can be overriden by calling the extension method that takes a success mapping function.
The defaults can be overriden by calling the extension method that takes a success mapping function.
A common example of when this is used is in a `POST` action when an entity has been created and we would like to return a 201 Created response to the client.

```csharp
Expand All @@ -48,8 +48,9 @@ public async Task<IActionResult> CreateFoo(NewFoo newFoo)

The `CreateFoo` method performs the domain logic to create a new `Foo` and returns `Result<Foo>`.

*In a real application it would be defined in the domain model project.
To give the domain model an API which is defined in terms of commands and queries and to decouple it from the outer application layers the mediator pattern is often adopted.
*In a real application it would be defined in the domain model project.
To give the domain model an API which is defined in terms of commands and queries and to decouple it from the outer application layers the mediator pattern is often adopted.

Jimmy Bogard's [MediatR](https://github.com/jbogard/MediatR) is a useful library for implementing that pattern.*

### Failure Result Mappings
Expand All @@ -66,13 +67,13 @@ The following table shows the default mappings.

_*This includes any other types that inherit from `Error` and are not explicitly listed._

The defaults can be overriden by calling the extension method that takes an error mapping function.
The defaults can be overriden by calling the extension method that takes an error mapping function.
This is useful when the domain model has defined additional error types and these need to be converted to the relevant problem details.
The status code that is set on the `ProblemDetails` will also be set on the `IActionResult` by the extension method so that the HTTP status code on the response is correct.

For example consider a domain model that deals with payments.
It could be a news service which requires a subscription to access content.
It might contain several operations that require payment to be made before they can proceed.
For example consider a domain model that deals with payments.
It could be a news service which requires a subscription to access content.
It might contain several operations that require payment to be made before they can proceed.
This domain may therefore define a new error type as follows:

```csharp
Expand All @@ -85,7 +86,7 @@ public class PaymentRequired : Error
}
```

It would therefore make sense to map this to a `402 Payment Required` HTTP response with relevant `ProblemDetails`.
It would therefore make sense to map this to a `402 Payment Required` HTTP response with relevant `ProblemDetails`.
This can be achieved like so:

```csharp
Expand All @@ -105,10 +106,10 @@ public async Task<IActionResult> GetNewsItem(string id)
}
```

The type field should return a URI that resolves to human-readable documentation about the type of error that has occurred.
This can either be existing documentation, such as https://httpstatuses.com for common errors, or your own documentation for domain-specific errors.
The type field should return a URI that resolves to human-readable documentation about the type of error that has occurred.
This can either be existing documentation, such as [https://httpstatuses.com](https://httpstatuses.com) for common errors, or your own documentation for domain-specific errors.

Problem details is formally documented in [RFC 7807](https://tools.ietf.org/html/rfc7807).
Problem details is formally documented in [RFC 7807](https://tools.ietf.org/html/rfc7807).
More information about how the fields should be used can be found there.

In order to maintain a loose coupling between the API layer and the domain model each action method should know how to map any kind of domain error.
Expand All @@ -135,8 +136,8 @@ internal static ProblemDetails MapDomainErrors(Error error)
}
```

By using C# pattern matching we can easily match on the type of error and map it to a `ProblemDetails`.
By using C# pattern matching we can easily match on the type of error and map it to a `ProblemDetails`.
Returning `null` in the default case means the existing error mappings for the common error types, as defined above, are used.

If you have a custom error type and you are happy for your REST API to return `400 Bad Request` when it occurs, then the default error mappings for the base `Error` type should already work for you.
If you have a custom error type and you are happy for your REST API to return `400 Bad Request` when it occurs, then the default error mappings for the base `Error` type should already work for you.
It maps the error's details and title to the corresponding fields on the problem details.
27 changes: 0 additions & 27 deletions appveyor.yml

This file was deleted.

62 changes: 0 additions & 62 deletions src/Winton.DomainModelling.AspNetCore/DomainExceptionFilter.cs

This file was deleted.

50 changes: 24 additions & 26 deletions src/Winton.DomainModelling.AspNetCore/ErrorExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,33 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace Winton.DomainModelling.AspNetCore
namespace Winton.DomainModelling.AspNetCore;
internal static class ErrorExtensions
{
internal static class ErrorExtensions
internal static ActionResult ToActionResult(this Error error, Func<Error, ProblemDetails?>? selectProblemDetails)
{
internal static ActionResult ToActionResult(this Error error, Func<Error, ProblemDetails?>? selectProblemDetails)
ProblemDetails problemDetails = selectProblemDetails?.Invoke(error) ?? CreateDefaultProblemDetails(error);
return new ObjectResult(problemDetails)
{
ProblemDetails problemDetails = selectProblemDetails?.Invoke(error) ?? CreateDefaultProblemDetails(error);
return new ObjectResult(problemDetails)
{
StatusCode = problemDetails.Status
};
}
StatusCode = problemDetails.Status
};
}

private static ProblemDetails CreateDefaultProblemDetails(Error error)
private static ProblemDetails CreateDefaultProblemDetails(Error error)
{
int statusCode = error switch
{
UnauthorizedError _ => StatusCodes.Status403Forbidden,
NotFoundError _ => StatusCodes.Status404NotFound,
ConflictError _ => StatusCodes.Status409Conflict,
_ => StatusCodes.Status400BadRequest
};
return new ProblemDetails
{
int statusCode = error switch
{
UnauthorizedError _ => StatusCodes.Status403Forbidden,
NotFoundError _ => StatusCodes.Status404NotFound,
ConflictError _ => StatusCodes.Status409Conflict,
_ => StatusCodes.Status400BadRequest
};
return new ProblemDetails
{
Detail = error.Detail,
Status = statusCode,
Title = error.Title,
Type = $"https://httpstatuses.com/{statusCode}"
};
}
Detail = error.Detail,
Status = statusCode,
Title = error.Title,
Type = $"https://httpstatuses.com/{statusCode}"
};
}
}
}
Loading

0 comments on commit 0b449a4

Please sign in to comment.