Skip to content

Commit

Permalink
Merge pull request #3 from marcominerva/develop
Browse files Browse the repository at this point in the history
Add OperationResults for ASP.NET Core
  • Loading branch information
marcominerva committed Sep 16, 2022
2 parents 5538d53 + d1e7732 commit b025c45
Show file tree
Hide file tree
Showing 12 changed files with 365 additions and 4 deletions.
50 changes: 50 additions & 0 deletions .github/workflows/publish_aspnetcore.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Publish OperationResults for ASP.NET Core on NuGet

on:
push:
branches: [ master ]
paths: [ 'src/OperationResults.AspNetCore/**' ]
workflow_dispatch:

env:
NET_VERSION: '6.x'
PROJECT_NAME: src/OperationResults.AspNetCore
PROJECT_FILE: OperationResults.AspNetCore.csproj

jobs:
build:
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@v1
with:
dotnet-version: ${{ env.NET_VERSION }}

- name: Nerdbank.GitVersioning
uses: dotnet/nbgv@v0.4
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: aspnetcore_v${{ steps.nbgv.outputs.NuGetPackageVersion }}
release_name: ASP.NET Core Release v${{ steps.nbgv.outputs.NuGetPackageVersion }}
draft: false
prerelease: false
8 changes: 7 additions & 1 deletion OperationResults.sln
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{40EA0531-BCD9-4D33-9114-D1BB9EA7DDBA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OperationResults.WebApi", "samples\OperationResults.WebApi\OperationResults.WebApi.csproj", "{11C60254-4212-42ED-ACDF-CAC830E8BEB8}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OperationResults.WebApi", "samples\OperationResults.WebApi\OperationResults.WebApi.csproj", "{11C60254-4212-42ED-ACDF-CAC830E8BEB8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OperationResults.AspNetCore", "src\OperationResults.AspNetCore\OperationResults.AspNetCore.csproj", "{375F6E50-F06A-4832-A712-D2C7A195DE64}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand All @@ -28,6 +30,10 @@ Global
{11C60254-4212-42ED-ACDF-CAC830E8BEB8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{11C60254-4212-42ED-ACDF-CAC830E8BEB8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{11C60254-4212-42ED-ACDF-CAC830E8BEB8}.Release|Any CPU.Build.0 = Release|Any CPU
{375F6E50-F06A-4832-A712-D2C7A195DE64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{375F6E50-F06A-4832-A712-D2C7A195DE64}.Debug|Any CPU.Build.0 = Debug|Any CPU
{375F6E50-F06A-4832-A712-D2C7A195DE64}.Release|Any CPU.ActiveCfg = Release|Any CPU
{375F6E50-F06A-4832-A712-D2C7A195DE64}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc;
using OperationResults.AspNetCore;
using OperationResults.WebApi.Services;

namespace OperationResults.WebApi.Controllers;
Expand All @@ -19,6 +20,7 @@ public async Task<IActionResult> GetList([FromQuery(Name = "q")] string queryTex
{
var result = await peopleService.GetAsync(queryText);

return Ok(result);
var response = HttpContext.CreateResponse(result);
return response;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\OperationResults.AspNetCore\OperationResults.AspNetCore.csproj" />
<ProjectReference Include="..\..\src\OperationResults\OperationResults.csproj" />
</ItemGroup>

Expand Down
9 changes: 9 additions & 0 deletions samples/OperationResults.WebApi/Program.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using OperationResults;
using OperationResults.AspNetCore;
using OperationResults.WebApi.Services;

var builder = WebApplication.CreateBuilder(args);
Expand All @@ -8,6 +10,13 @@

builder.Services.AddScoped<PeopleService>();

builder.Services.AddOperationResult(options =>
{
options.ErrorResponseFormat = ErrorResponseFormat.Default;
//options.StatusCodesMapping.Add(42, StatusCodes.Status406NotAcceptable);
options.StatusCodesMapping[FailureReasons.DatabaseError] = StatusCodes.Status502BadGateway;
}, true, "Errori di validazione");

// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
Expand Down
10 changes: 8 additions & 2 deletions samples/OperationResults.WebApi/Services/PeopleService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@ public class PeopleService
public async Task<Result<IEnumerable<Person>>> GetAsync(string queryText)
{
await Task.Delay(100);
var people = new List<Person>();
_ = new List<Person>();

return people;
var errors = new List<ValidationError>
{
new(nameof(Person.FirstName), "Il nome è obbligatorio"),
new(nameof(Person.LastName), "Il cognome è obbligatorio")
};

return Result.Fail(FailureReasons.ClientError, errors);
}

public async Task<Result<Person>> GetAsync(Guid id)
Expand Down
127 changes: 127 additions & 0 deletions src/OperationResults.AspNetCore/ControllerExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.DependencyInjection;

namespace OperationResults.AspNetCore;

public static class ControllerExtensions
{
public static IActionResult CreateResponse(this HttpContext httpContext, Result operationResult, int? responseStatusCode = null)
{
if (operationResult.Success)
{
return new StatusCodeResult(responseStatusCode.GetValueOrDefault(StatusCodes.Status204NoContent));
}

return Problem(httpContext, FailureReasonToStatusCode(httpContext, operationResult.FailureReason), null, operationResult.ErrorMessage, operationResult.ErrorDetail, operationResult.ValidationErrors);
}

public static IActionResult CreateResponse<T>(this HttpContext httpContext, Result<T> operationResult, int? responseStatusCode = null)
=> CreateResponse(httpContext, operationResult, null, null, responseStatusCode);

public static IActionResult CreateResponse<T>(this HttpContext httpContext, Result<T> operationResult, string? actionName, object? routeValues = null, int? responseStatusCode = null)
{
if (operationResult.Success)
{
if (operationResult.Content is not null)
{
if (!string.IsNullOrWhiteSpace(actionName))
{
var routeValueDictionary = new RouteValueDictionary(routeValues);
//var apiVersion = HttpContext.GetRequestedApiVersion();
//if (!routeValueDictionary.ContainsKey("version") && apiVersion != null)
//{
// routeValueDictionary.Add("version", apiVersion.ToString());
//}

return new CreatedAtRouteResult(actionName, routeValueDictionary, operationResult.Content);
}
else if (operationResult.Content is StreamFileContent streamFileContent)
{
var fileStreamResult = new FileStreamResult(streamFileContent.Content, streamFileContent.ContentType)
{
FileDownloadName = streamFileContent.DownloadFileName
};

return fileStreamResult;
}
else if (operationResult.Content is ByteArrayFileContent byteArrayFileContent)
{
var fileContentResult = new FileContentResult(byteArrayFileContent.Content, byteArrayFileContent.ContentType)
{
FileDownloadName = byteArrayFileContent.DownloadFileName
};

return fileContentResult;
}

var okResult = new ObjectResult(operationResult.Content)
{
StatusCode = responseStatusCode.GetValueOrDefault(StatusCodes.Status200OK)
};

return okResult;
}

return new StatusCodeResult(responseStatusCode.GetValueOrDefault(StatusCodes.Status204NoContent));
}

return Problem(httpContext, FailureReasonToStatusCode(httpContext, operationResult.FailureReason), operationResult.Content, operationResult.ErrorMessage, operationResult.ErrorDetail, operationResult.ValidationErrors);
}

private static IActionResult Problem(HttpContext httpContext, int statusCode, object? content = null, string? title = null, string? detail = null, IEnumerable<ValidationError>? validationErrors = null)
{
if (content is not null)
{
var objectResult = new ObjectResult(content)
{
StatusCode = statusCode
};

return objectResult;
}

var problemDetails = new ProblemDetails
{
Status = statusCode,
Type = $"https://httpstatuses.io/{statusCode}",
Title = title ?? ReasonPhrases.GetReasonPhrase(statusCode),
Detail = detail,
Instance = httpContext.Request.Path
};

problemDetails.Extensions.Add("traceId", Activity.Current?.Id ?? httpContext.TraceIdentifier);
if (validationErrors?.Any() ?? false)
{
var options = httpContext.RequestServices.GetRequiredService<OperationResultOptions>();

if (options.ErrorResponseFormat == ErrorResponseFormat.Default)
{
var errors = validationErrors.GroupBy(v => v.Name).ToDictionary(k => k.Key, v => v.Select(e => e.Message));
problemDetails.Extensions.Add("errors", errors);
}
else
{
problemDetails.Extensions.Add("errors", validationErrors);
}
}

var problemDetailsResults = new ObjectResult(problemDetails)
{
StatusCode = statusCode
};

return problemDetailsResults;
}

private static int FailureReasonToStatusCode(HttpContext httpContext, int failureReason, int? defaultResponseStatusCode = null)
{
var options = httpContext.RequestServices.GetRequiredService<OperationResultOptions>();
var statusCode = options.GetStatusCode(failureReason, defaultResponseStatusCode);

return statusCode;
}
}
7 changes: 7 additions & 0 deletions src/OperationResults.AspNetCore/ErrorResponseFormat.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace OperationResults.AspNetCore;

public enum ErrorResponseFormat
{
Default,
List
}
36 changes: 36 additions & 0 deletions src/OperationResults.AspNetCore/OperationResultOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Microsoft.AspNetCore.Http;

namespace OperationResults.AspNetCore;

public class OperationResultOptions
{
public ErrorResponseFormat ErrorResponseFormat { get; set; }

public Dictionary<int, int> StatusCodesMapping { get; set; }

public OperationResultOptions()
{
StatusCodesMapping = new Dictionary<int, int>
{
[FailureReasons.None] = StatusCodes.Status200OK,
[FailureReasons.ItemNotFound] = StatusCodes.Status404NotFound,
[FailureReasons.Forbidden] = StatusCodes.Status403Forbidden,
[FailureReasons.DatabaseError] = StatusCodes.Status500InternalServerError,
[FailureReasons.ClientError] = StatusCodes.Status400BadRequest,
[FailureReasons.InvalidFile] = StatusCodes.Status415UnsupportedMediaType,
[FailureReasons.Conflict] = StatusCodes.Status409Conflict,
[FailureReasons.Unauthorized] = StatusCodes.Status401Unauthorized,
[FailureReasons.GenericError] = StatusCodes.Status500InternalServerError
};
}

internal int GetStatusCode(int failureReason, int? defaultStatusCode = null)
{
if (!StatusCodesMapping.TryGetValue(failureReason, out var statusCode))
{
statusCode = defaultStatusCode.GetValueOrDefault(StatusCodes.Status501NotImplemented);
}

return statusCode;
}
}
42 changes: 42 additions & 0 deletions src/OperationResults.AspNetCore/OperationResults.AspNetCore.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Authors>Marco Minerva</Authors>
<Company>Marco Minerva</Company>
<Product>OperationResults.AspNetCore</Product>
<Title>OperationResults.AspNetCore</Title>
<Description>A lightweight library to totally decouple operation results and ASP.NET Core responses.</Description>
<PackageId>OperationResultTools.AspNetCore</PackageId>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/marcominerva/OperationResults</PackageProjectUrl>
<PackageIcon>Toolbox.png</PackageIcon>
<PackageTags>csharp visual-studio net aspnetcore desktop web mobile utilities helpers</PackageTags>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/marcominerva/OperationResults.git</RepositoryUrl>
<RepositoryBranch>master</RepositoryBranch>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Nerdbank.GitVersioning" Version="3.5.109">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="OperationResultTools" Version="1.0.2" />
</ItemGroup>

<ItemGroup>
<None Include="..\..\Toolbox.png">
<Pack>True</Pack>
<PackagePath></PackagePath>
</None>
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
</ItemGroup>
</Project>
Loading

0 comments on commit b025c45

Please sign in to comment.