Skip to content

Commit

Permalink
Fix 179 (#180)
Browse files Browse the repository at this point in the history
* Working on fix for 179

* merge

* Fix build warnings

* Fixing tests
Added fix for 179
Added test to confirm fix
  • Loading branch information
ardalis authored May 15, 2024
1 parent ddbac3f commit 54b5de8
Show file tree
Hide file tree
Showing 21 changed files with 149 additions and 39 deletions.
27 changes: 13 additions & 14 deletions sample/Ardalis.Result.Sample.Core/Ardalis.Result.Sample.Core.csproj
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<AssemblyName>Ardalis.Result.Sample.Core</AssemblyName>
<RootNamespace>Ardalis.Result.Sample.Core</RootNamespace>
</PropertyGroup>
<PropertyGroup>
<AssemblyName>Ardalis.Result.Sample.Core</AssemblyName>
<RootNamespace>Ardalis.Result.Sample.Core</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" />
<PackageReference Include="System.ComponentModel.Annotations" />
<PackageReference Include="System.Runtime" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Ardalis.Result.FluentValidation\Ardalis.Result.FluentValidation.csproj" />
<ProjectReference Include="..\..\src\Ardalis.Result\Ardalis.Result.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" />
<PackageReference Include="System.ComponentModel.Annotations" />
<PackageReference Include="System.Runtime" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Ardalis.Result.FluentValidation\Ardalis.Result.FluentValidation.csproj" />
<ProjectReference Include="..\..\src\Ardalis.Result\Ardalis.Result.csproj" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
namespace Ardalis.Result.Sample.Core.DTOs;
using System;

namespace Ardalis.Result.Sample.Core.DTOs;

public class CreatePersonRequestDto
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string FirstName { get; set; } = String.Empty;
public string LastName { get; set; } = String.Empty;
}
3 changes: 2 additions & 1 deletion sample/Ardalis.Result.Sample.Core/DTOs/ForecastRequestDto.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@

using System;
using System.ComponentModel.DataAnnotations;

namespace Ardalis.Result.Sample.Core.DTOs
{
public class ForecastRequestDto
{
[Required]
public string PostalCode { get; set; }
public string PostalCode { get; set; } = String.Empty;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Ardalis.Result.Sample.Core.Exceptions
{
public class ForecastRequestInvalidException : Exception
{
public Dictionary<string,string> ValidationErrors { get; set; }
public Dictionary<string,string> ValidationErrors { get; set; } = new();

public ForecastRequestInvalidException(Dictionary<string, string> validationErrors) : base("Forecast request is invalid.")
{
Expand Down
6 changes: 3 additions & 3 deletions sample/Ardalis.Result.Sample.Core/Model/Person.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ namespace Ardalis.Result.Sample.Core.Model
public class Person
{
public int Id { get; set; }
public string Surname { get; set; }
public string Forename { get; set; }
public string Surname { get; set; } = String.Empty;
public string Forename { get; set; } = String.Empty;

public List<Person> Children { get; set; }
public List<Person> Children { get; set; } = new();

public DateTime DateOfBirth { get; set; }
}
Expand Down
2 changes: 1 addition & 1 deletion sample/Ardalis.Result.Sample.Core/Model/WeatherForecast.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ public class WeatherForecast

public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);

public string Summary { get; set; }
public string Summary { get; set; } = String.Empty;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public async Task ReturnsOkWithValueGivenValidPostalCode()
var stringResponse = await response.Content.ReadAsStringAsync();
var forecasts = JsonConvert.DeserializeObject<List<WeatherForecast>>(stringResponse);

Assert.Equal("Freezing", forecasts.First().Summary);
Assert.Equal("Freezing", forecasts?.First()?.Summary);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,30 @@

namespace Ardalis.Result.SampleWeb.FunctionalTests;

public class WeatherForecastControllerThrows : IClassFixture<WebApplicationFactory<WebMarker>>
{
private const string CONTROLLER_THROWS_ROUTE = "/weatherforecast/throws";
private readonly HttpClient _client;

public WeatherForecastControllerThrows(WebApplicationFactory<WebMarker> factory)
{
_client = factory.CreateClient();
}

[Fact]
public async Task Returns400BadRequestNot500()
{
var response = await _client.GetAsync(CONTROLLER_THROWS_ROUTE);

Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var stringResponse = await response.Content.ReadAsStringAsync();
var problemDetails = JsonConvert.DeserializeObject<ProblemDetails>(stringResponse);

Assert.Equal("One or more validation errors occurred.", problemDetails?.Title);
Assert.Equal(400, problemDetails.Status);

Check warning on line 36 in sample/Ardalis.Result.SampleWeb.FunctionalTests/WeatherForecastControllerPost.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

Check warning on line 36 in sample/Ardalis.Result.SampleWeb.FunctionalTests/WeatherForecastControllerPost.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

Check warning on line 36 in sample/Ardalis.Result.SampleWeb.FunctionalTests/WeatherForecastControllerPost.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
}
}

public class WeatherForecastControllerPost : IClassFixture<WebApplicationFactory<WebMarker>>
{
private const string CONTROLLER_POST_ROUTE = "/weatherforecast/create";
Expand All @@ -36,7 +60,7 @@ public async Task ReturnsOkWithValueGivenValidPostalCode(string route)
var stringResponse = await response.Content.ReadAsStringAsync();
var forecasts = JsonConvert.DeserializeObject<List<WeatherForecast>>(stringResponse);

Assert.Equal("Freezing", forecasts.First().Summary);
Assert.Equal("Freezing", forecasts?.First()?.Summary);
}

[Theory]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public Task<Result<IEnumerable<WeatherForecast>>> CreateForecast([FromBody] NewF
public class NewForecastCommand : IRequest<Result<IEnumerable<WeatherForecast>>>
{
[Required]
public string PostalCode { get; set; }
public string PostalCode { get; set; } = String.Empty;
}

public class NewForecastHandler : IRequestHandler<NewForecastCommand, Result<IEnumerable<WeatherForecast>>>
Expand Down
2 changes: 1 addition & 1 deletion sample/Ardalis.Result.SampleWeb/Pages/Index.cshtml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public IndexModel(IStringLocalizer<IndexModel> stringLocalizer)
_stringLocalizer = stringLocalizer;
}

public string Message { get; set; }
public string Message { get; set; } = String.Empty;
public void OnGet()
{
Message = _stringLocalizer["message"].Value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,15 @@ public ActionResult<WeatherForecastSummaryDto> CreateSummaryForecast([FromBody]
// .Map(WeatherForecastSummaryDto.MapFrom)
.ToActionResult(this);
}

/// <summary>
/// Issue #179
/// </summary>
/// <returns></returns>
[HttpGet("throws")]
[TranslateResultToActionResult]
public Result<int> Throws()
{
return Result<int>.Invalid(new ValidationError("foo"));
}
}
2 changes: 1 addition & 1 deletion sample/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<NetCoreFrameworks>net6.0;net7.0;net8.0</NetCoreFrameworks>
<TargetFrameworks>net4.7.1;$(NetCoreFrameworks)</TargetFrameworks>
<TargetFrameworks>net48;$(NetCoreFrameworks)</TargetFrameworks>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
Expand Down
6 changes: 5 additions & 1 deletion src/Ardalis.Result.AspNetCore/ResultStatusMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,11 @@ private static ValidationProblemDetails BadRequest(ControllerBase controller, IR
{
foreach (var error in result.ValidationErrors)
{
controller.ModelState.AddModelError(error.Identifier, error.ErrorMessage);
// TODO: mark ValidationError.Identifier as required and limit setting (see #179)
string identifier = error.Identifier ?? "(identifier)";
// TODO: mark ValidationError.Identifier as required and limit setting (see #179)
string errorMessage = error.ErrorMessage ?? "(error message)";
controller.ModelState.AddModelError(identifier, errorMessage);
}

return new ValidationProblemDetails(controller.ModelState);
Expand Down
1 change: 1 addition & 0 deletions src/Ardalis.Result/ValidationError.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public ValidationError(string identifier, string errorMessage, string errorCode,
}

public string Identifier { get; set; }
// TODO: Mark required and limit setting (see #179)
public string ErrorMessage { get; set; }
public string ErrorCode { get; set; }
public ValidationSeverity Severity { get; set; } = ValidationSeverity.Error;
Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<NetCoreFrameworks>net6.0;net7.0;net8.0</NetCoreFrameworks>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<TargetFrameworks>netstandard2.0;$(NetCoreFrameworks)</TargetFrameworks>
<LangVersion>latest</LangVersion>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Authors>Steve Smith (@ardalis); Shady Nagy (@ShadyNagy)</Authors>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System.Reflection;

namespace Ardalis.Result.AspNetCore.UnitTests;

public class BaseResultConventionTest
{
protected bool ProducesResponseTypeAttribute(IFilterMetadata filterMetadata, int statusCode, Type type)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,10 @@ public void ResultWithValue(string actionName, Type expectedType)

convention.Apply(actionModel);

Assert.Equal(9, actionModel.Filters.Where(f => f is ProducesResponseTypeAttribute).Count());
Assert.Equal(10, actionModel.Filters.Where(f => f is ProducesResponseTypeAttribute).Count());

Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 200, expectedType));
Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 204, typeof(void)));
Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 404, typeof(ProblemDetails)));
Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 400, typeof(ValidationProblemDetails)));
Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 401, typeof(void)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,8 @@ public void ChangeResultStatus()
[InlineData("Index", typeof(void), typeof(HttpPostAttribute), 204)]
[InlineData("Index", typeof(void), typeof(HttpDeleteAttribute), 204)]
[InlineData("Index", typeof(void), typeof(HttpGetAttribute), 204)]
[InlineData(nameof(TestController.ResultString), typeof(string), typeof(HttpPostAttribute), 201)]
[InlineData(nameof(TestController.ResultString), typeof(string), typeof(HttpDeleteAttribute), 204)]
[InlineData(nameof(TestController.ResultString), typeof(string), typeof(HttpGetAttribute), 200)]
[InlineData(nameof(TestController.ResultEnumerableString), typeof(IEnumerable<string>), typeof(HttpPostAttribute), 201)]
[InlineData(nameof(TestController.ResultEnumerableString), typeof(IEnumerable<string>), typeof(HttpDeleteAttribute), 204)]
[InlineData(nameof(TestController.ResultEnumerableString), typeof(IEnumerable<string>), typeof(HttpGetAttribute), 200)]
public void ChangeResultStatus_ForSpecificMethods(string actionName, Type expectedType, Type attributeType, int expectedStatusCode)
{
var convention = new ResultConvention(new ResultStatusMap()
Expand Down Expand Up @@ -96,4 +92,39 @@ public void ChangeResultStatus_ForSpecificMethods(string actionName, Type expect
Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 500, typeof(ProblemDetails)));
Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 503, typeof(ProblemDetails)));
}

[Theory]
[InlineData(nameof(TestController.ResultString), typeof(string), typeof(HttpPostAttribute), 201)]
[InlineData(nameof(TestController.ResultString), typeof(string), typeof(HttpGetAttribute), 200)]
[InlineData(nameof(TestController.ResultEnumerableString), typeof(IEnumerable<string>), typeof(HttpPostAttribute), 201)]
[InlineData(nameof(TestController.ResultEnumerableString), typeof(IEnumerable<string>), typeof(HttpGetAttribute), 200)]
public void ChangeResultStatus_ForDeleteMethods(string actionName, Type expectedType, Type attributeType, int expectedStatusCode)
{
var convention = new ResultConvention(new ResultStatusMap()
.AddDefaultMap()
.For(ResultStatus.Ok, HttpStatusCode.OK, opts => opts
.For("POST", HttpStatusCode.Created)
.For("DELETE", HttpStatusCode.NoContent)));

var actionModelBuilder = new ActionModelBuilder()
.AddActionFilter(new TranslateResultToActionResultAttribute())
.AddActionAttribute((Attribute)Activator.CreateInstance(attributeType)!);

var actionModel = actionModelBuilder.GetActionModel(actionName);

convention.Apply(actionModel);

Assert.Equal(10, actionModel.Filters.Where(f => f is ProducesResponseTypeAttribute).Count());

Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, expectedStatusCode, expectedType));
Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 204, typeof(void)));
Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 404, typeof(ProblemDetails)));
Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 400, typeof(ValidationProblemDetails)));
Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 401, typeof(void)));
Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 403, typeof(void)));
Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 409, typeof(ProblemDetails)));
Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 422, typeof(ProblemDetails)));
Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 500, typeof(ProblemDetails)));
Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 503, typeof(ProblemDetails)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Microsoft.AspNetCore.Mvc;
using Xunit;

namespace Ardalis.Result.AspNetCore.UnitTests;

public class ResultStatusMapAddDefaultMap
{
[Fact]
public void ReturnsBadRequestGivenInvalid()
{
var controller = new TestController();

var result = Result<int>.Invalid(new ValidationError("test"));

// TODO: require identifier
Assert.True(true);
}

public class TestController : ControllerBase
{
public Result Index()
{
throw new NotImplementedException();
}

public Result<string> ResultString()
{
throw new NotImplementedException();
}

public Result<IEnumerable<string>> ResultEnumerableString()
{
throw new NotImplementedException();
}
}

}
4 changes: 2 additions & 2 deletions tests/Ardalis.Result.UnitTests/SystemTextJsonSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public class SystemTextJsonSerializer
public void ShouldSerializeResultOfValueType()
{
var result = Result.Success(5);
string expected = "{\"Value\":5,\"Status\":0,\"IsSuccess\":true,\"SuccessMessage\":\"\",\"CorrelationId\":\"\",\"Errors\":[],\"ValidationErrors\":[]}";
string expected = "{\"Value\":5,\"Status\":0,\"IsSuccess\":true,\"SuccessMessage\":\"\",\"CorrelationId\":\"\",\"Location\":\"\",\"Errors\":[],\"ValidationErrors\":[]}";

var json = JsonSerializer.Serialize(result);

Expand All @@ -20,7 +20,7 @@ public void ShouldSerializeResultOfValueType()
public void ShouldSerializeResultOfReferenceType()
{
var result = Result.Success(new Foo { Bar = "Result!" });
string expected = "{\"Value\":{\"Bar\":\"Result!\"},\"Status\":0,\"IsSuccess\":true,\"SuccessMessage\":\"\",\"CorrelationId\":\"\",\"Errors\":[],\"ValidationErrors\":[]}";
string expected = "{\"Value\":{\"Bar\":\"Result!\"},\"Status\":0,\"IsSuccess\":true,\"SuccessMessage\":\"\",\"CorrelationId\":\"\",\"Location\":\"\",\"Errors\":[],\"ValidationErrors\":[]}";

var json = JsonSerializer.Serialize(result);

Expand Down
2 changes: 1 addition & 1 deletion tests/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<NetCoreFrameworks>net6.0;net7.0;net8.0</NetCoreFrameworks>
<TargetFrameworks>net4.7.1;$(NetCoreFrameworks)</TargetFrameworks>
<TargetFrameworks>net48;$(NetCoreFrameworks)</TargetFrameworks>
<IsPackable>false</IsPackable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
Expand Down

0 comments on commit 54b5de8

Please sign in to comment.