Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Ardalis.Result.Sample.Core.DTOs;

public class CreatePersonRequestDto
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System;

namespace Ardalis.Result.Sample.Core.Exceptions
{
public class ForecastConflictException : Exception
{
public ForecastConflictException() : base("Forecast Conflict.") { }
}
}
14 changes: 11 additions & 3 deletions sample/Ardalis.Result.Sample.Core/Services/PersonService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ namespace Ardalis.Result.Sample.Core.Services
public class PersonService
{
private readonly int[] _knownIds = new [] { 1 };
private readonly Person _existPerson = new() { Forename = "John", Surname = "Smith" };

public Result<Person> Create(string firstName, string lastName)
{
var person = new Person();
person.Forename = firstName;
person.Surname = lastName;
var person = new Person
{
Forename = firstName,
Surname = lastName
};

var validator = new PersonValidator();

Expand All @@ -23,6 +26,11 @@ public Result<Person> Create(string firstName, string lastName)
return Result.Invalid(result.AsErrors());
}

if (person.Forename == _existPerson.Forename && person.Surname == _existPerson.Surname)
{
return Result.Conflict($"Person ({person.Forename} {person.Surname}) is exist");
}

return Result.Success(person);
}

Expand Down
16 changes: 14 additions & 2 deletions sample/Ardalis.Result.Sample.Core/Services/WeatherService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@ public Task<Result<IEnumerable<WeatherForecast>>> GetForecastAsync(ForecastReque

public Result<IEnumerable<WeatherForecast>> GetForecast(ForecastRequestDto model)
{
if (model.PostalCode == "NotFound") return Result.NotFound();
switch (model.PostalCode)
{
case "NotFound":
return Result.NotFound();
case "Conflict":
return Result.Conflict();
}

// validate model
if (model.PostalCode.Length > 10)
Expand Down Expand Up @@ -77,7 +83,13 @@ public Result<IEnumerable<WeatherForecast>> GetForecast(ForecastRequestDto model

public Result<WeatherForecast> GetSingleForecast(ForecastRequestDto model)
{
if (model.PostalCode == "NotFound") return Result.NotFound();
switch (model.PostalCode)
{
case "NotFound":
return Result.NotFound();
case "Conflict":
return Result.Conflict();
}

// validate model
if (model.PostalCode.Length > 10)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@ public Task<IEnumerable<WeatherForecast>> GetForecastAsync(ForecastRequestDto mo

public IEnumerable<WeatherForecast> GetForecast(ForecastRequestDto model)
{
if (model.PostalCode == "NotFound") throw new ForecastNotFoundException();
switch (model.PostalCode)
{
case "NotFound":
throw new ForecastNotFoundException();
case "Conflict":
throw new ForecastConflictException();
}

// validate model
if (model.PostalCode.Length > 10)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Linq;
using Ardalis.Result.Sample.Core.Services;
using FluentAssertions;
using Xunit;
Expand Down Expand Up @@ -28,4 +29,16 @@ public void ReturnsInvalidResultWith2ErrorsGivenSomeLongNameSurname()
result.ValidationErrors.Count.Should().Be(2);
}

[Fact]
public void ReturnsConflictResultGivenExistPerson()
{
var service = new PersonService();
string firstName = "John";
string lastName = "Smith";

var result = service.Create(firstName, lastName);

result.Status.Should().Be(ResultStatus.Conflict);
result.Errors.Single().Should().Be($"Person ({firstName} {lastName}) is exist");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,14 @@ public async Task ReturnsBadRequestGivenPostalCodeTooLong()
var stringResponse = await response.Content.ReadAsStringAsync();
Assert.Contains("PostalCode cannot exceed 10 characters.", stringResponse);
}

[Fact]
public async Task ReturnsConflictGivenNonExistentPostalCode()
{
var requestDto = new ForecastRequestDto() { PostalCode = "Conflict" };
var jsonContent = new StringContent(JsonConvert.SerializeObject(requestDto), Encoding.Default, "application/json");
var response = await _client.PostAsync(ENDPOINT_POST_ROUTE, jsonContent);

Assert.Equal(HttpStatusCode.Conflict, response.StatusCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Testing;
using Newtonsoft.Json;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Ardalis.Result.Sample.Core.DTOs;
using Xunit;

namespace Ardalis.Result.SampleWeb.FunctionalTests;

public class PersonControllerCreate : IClassFixture<WebApplicationFactory<WebMarker>>
{
private const string MEDIATR_CONTROLLER_POST_ROUTE = "/mediatr/person/create/";
private const string CONTROLLER_POST_ROUTE = "/person/new/";
private const string ENDPOINT_POST_ROUTE = "/person/create/";

private readonly HttpClient _client;

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

[Theory]
[InlineData(MEDIATR_CONTROLLER_POST_ROUTE)]
[InlineData(CONTROLLER_POST_ROUTE)]
[InlineData(ENDPOINT_POST_ROUTE)]
public async Task ReturnsConflictGivenExistPerson(string route)
{
var firstName = "John";
var lastName = "Smith";
var response = await SendCreateRequest(route, firstName, lastName);

Assert.Equal(HttpStatusCode.Conflict, response.StatusCode);
var stringResponse = await response.Content.ReadAsStringAsync();

var problemDetails = JsonConvert.DeserializeObject<ProblemDetails>(stringResponse);

Assert.Contains("There was a conflict.", problemDetails.Title);
Assert.Contains("Next error(s) occured:* Person (John Smith) is exist\r\n", problemDetails.Detail);
Assert.Equal(409, problemDetails.Status);
}

private async Task<HttpResponseMessage> SendCreateRequest(string route, string firstName, string lastName)
{
var createPersonRequestDto = new CreatePersonRequestDto{ FirstName = firstName, LastName = lastName };
var json = JsonConvert.SerializeObject(createPersonRequestDto);
var data = new StringContent(json, Encoding.UTF8, "application/json");

return await _client.PostAsync(route, data);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
using Newtonsoft.Json;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Ardalis.Result.Sample.Core.DTOs;
using Xunit;

namespace Ardalis.Result.SampleWeb.FunctionalTests;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,23 @@ public async Task ReturnsBadRequestGivenPostalCodeTooLong(string route)
Assert.Contains(validationProblemDetails.Errors[nameof(ForecastRequestDto.PostalCode)], e => e.Equals("PostalCode cannot exceed 10 characters.", System.StringComparison.OrdinalIgnoreCase));
Assert.Equal(400, validationProblemDetails.Status);
}

[Theory]
[InlineData(CONTROLLER_POST_ROUTE)]
[InlineData(ENDPOINT_POST_ROUTE)]
public async Task ReturnsConflictGivenNonExistentPostalCode(string route)
{
var requestDto = new ForecastRequestDto() { PostalCode = "Conflict" };
var response = await PostDTOAndGetResponse(requestDto, route);

Assert.Equal(HttpStatusCode.Conflict, response.StatusCode);
var stringResponse = await response.Content.ReadAsStringAsync();

var problemDetails = JsonConvert.DeserializeObject<ProblemDetails>(stringResponse);

Assert.Equal("There was a conflict.", problemDetails.Title);
Assert.Equal(409, problemDetails.Status);
}

private async Task<HttpResponseMessage> PostDTOAndGetResponse(ForecastRequestDto dto, string route)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ public async Task<ActionResult<IEnumerable<WeatherForecast>>> CreateForecast([Fr
{
return NotFound();
}
catch (ForecastConflictException) // avoid using exceptions for control flow
{
return Conflict();
}
catch (ForecastRequestInvalidException ex) // avoid using exceptions for control flow
{
var dict = new ModelStateDictionary();
Expand Down
48 changes: 47 additions & 1 deletion sample/Ardalis.Result.SampleWeb/MediatrApi/PersonController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using Microsoft.AspNetCore.Mvc;
using System.Threading;
using System.Threading.Tasks;
using Ardalis.Result.Sample.Core.DTOs;
using Ardalis.Result.Sample.Core.Model;

namespace Ardalis.Result.SampleWeb.MediatrApi;

Expand All @@ -22,7 +24,6 @@ public PersonController(IMediator mediator)
/// This uses a filter to convert an Ardalis.Result return type to an ActionResult.
/// This filter could be used per controller or globally!
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[TranslateResultToActionResult]
[HttpDelete("Remove/{id}")]
Expand All @@ -34,6 +35,22 @@ public Task<Result> RemovePerson(int id)
return _mediator.Send(new RemovePersonCommand(id));
}

/// <summary>
/// This uses a filter to convert an Ardalis.Result return type to an ActionResult.
/// This filter could be used per controller or globally!
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[TranslateResultToActionResult]
[HttpPost("Create/")]
public Task<Result<Person>> CreatePerson(CreatePersonRequestDto request)
{
// One might try to perform translation from Result<T> to an appropriate IActionResult from within a MediatR pipeline
// Unfortunately without having Result<T> depend on IActionResult there doesn't appear to be a way to do this, so this
// example is still using the TranslateResultToActionResult filter.
return _mediator.Send(new CreatePersonCommand(request.FirstName, request.LastName));
}

public class RemovePersonCommand : IRequest<Result>
{
public RemovePersonCommand(int id)
Expand All @@ -60,4 +77,33 @@ public Task<Result> Handle(RemovePersonCommand request, CancellationToken cancel
return Task.FromResult(result);
}
}

public class CreatePersonCommand : IRequest<Result<Person>>
{
public CreatePersonCommand(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}

public string FirstName { get; set; }
public string LastName { get; set; }
}

public class CreatePersonCommandHandler : IRequestHandler<CreatePersonCommand, Result<Person>>
{
private readonly PersonService _personService;

public CreatePersonCommandHandler(PersonService personService)
{
_personService = personService;
}

public Task<Result<Person>> Handle(CreatePersonCommand request, CancellationToken cancellationToken)
{
var result = _personService.Create(request.FirstName, request.LastName);

return Task.FromResult(result);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Ardalis.ApiEndpoints;
using Ardalis.Result.AspNetCore;
using Ardalis.Result.Sample.Core.Services;
using Microsoft.AspNetCore.Mvc;
using System;
using Ardalis.Result.Sample.Core.DTOs;
using Ardalis.Result.Sample.Core.Model;

namespace Ardalis.Result.SampleWeb.PersonFeature;

public class CreatePersonEndpoint : EndpointBaseSync
.WithRequest<CreatePersonRequestDto>
.WithActionResult<Person>
{
private readonly PersonService _personService;

public CreatePersonEndpoint(PersonService personService)
{
_personService = personService;
}

/// <summary>
/// This uses an extension method to convert to an ActionResult
/// </summary>
/// <returns></returns>
[HttpPost("/Person/Create/")]
public override ActionResult<Person> Handle(CreatePersonRequestDto request)
{
if (DateTime.Now.Second % 2 == 0) // just so we can show both versions
{
// Extension method on ControllerBase
return this.ToActionResult(_personService.Create(request.FirstName, request.LastName));
}

Result<Person> result = _personService.Create(request.FirstName, request.LastName);

// Extension method on a Result instance (passing in ControllerBase instance)
return result.ToActionResult(this);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Ardalis.Result.AspNetCore;
using Ardalis.Result.Sample.Core.DTOs;
using Ardalis.Result.Sample.Core.Model;
using Ardalis.Result.Sample.Core.Services;
using Microsoft.AspNetCore.Mvc;

Expand All @@ -19,7 +21,6 @@ public PersonController(PersonService personService)
/// This uses a filter to convert an Ardalis.Result return type to an ActionResult.
/// This filter could be used per controller or globally!
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[TranslateResultToActionResult]
[ExpectedFailures(ResultStatus.NotFound, ResultStatus.Invalid)]
Expand All @@ -28,4 +29,17 @@ public Result RemovePerson(int id)
{
return _personService.Remove(id);
}

/// <summary>
/// This uses a filter to convert an Ardalis.Result return type to an ActionResult.
/// This filter could be used per controller or globally!
/// </summary>
/// <returns></returns>
[TranslateResultToActionResult]
[ExpectedFailures(ResultStatus.NotFound, ResultStatus.Invalid)]
[HttpPost("New/")]
public Result<Person> CreatePerson(CreatePersonRequestDto request)
{
return _personService.Create(request.FirstName, request.LastName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Ardalis.Result.Sample.Core.Services;
using Microsoft.AspNetCore.Mvc;
using System;
using Ardalis.Result.Sample.Core.Model;

namespace Ardalis.Result.SampleWeb.PersonFeature;

Expand All @@ -20,7 +21,6 @@ public PersonEndpoint(PersonService personService)
/// <summary>
/// This uses an extension method to convert to an ActionResult
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[HttpDelete("/Person/Delete/{id}")]
public override ActionResult Handle(int id)
Expand Down
Loading