Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Go functional experiment #14

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
9 changes: 9 additions & 0 deletions CleanArchitecture.sln
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.Applicati
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.Api.IntegrationTests", "tests\CleanArchitecture.Api.IntegrationTests\CleanArchitecture.Api.IntegrationTests.csproj", "{2A315D67-B631-478A-8F67-9EE9DB03D0C5}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ErrorOr", "ErrorOr", "{C47B3C2F-F488-4960-AAF6-A68BB9CB07E3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ErrorOr", "ErrorOr\src\ErrorOr.csproj", "{CBA12FED-3AB0-4276-A930-4AA691D595DE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -76,6 +80,10 @@ Global
{2A315D67-B631-478A-8F67-9EE9DB03D0C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2A315D67-B631-478A-8F67-9EE9DB03D0C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2A315D67-B631-478A-8F67-9EE9DB03D0C5}.Release|Any CPU.Build.0 = Release|Any CPU
{CBA12FED-3AB0-4276-A930-4AA691D595DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CBA12FED-3AB0-4276-A930-4AA691D595DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CBA12FED-3AB0-4276-A930-4AA691D595DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CBA12FED-3AB0-4276-A930-4AA691D595DE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{0B9F7D22-2A37-4EB6-A410-32BA01213D2A} = {E6C0A80B-5ADC-4138-A79D-C9C128EA163B}
Expand All @@ -88,5 +96,6 @@ Global
{9ED129C8-9C2F-4E19-8CCC-51FBC5CDE0A7} = {1DD25606-DF8E-4B91-9335-F916B14BF596}
{EC6267FA-4762-400A-8D7C-0FC0FB167AE8} = {1DD25606-DF8E-4B91-9335-F916B14BF596}
{2A315D67-B631-478A-8F67-9EE9DB03D0C5} = {1DD25606-DF8E-4B91-9335-F916B14BF596}
{CBA12FED-3AB0-4276-A930-4AA691D595DE} = {C47B3C2F-F488-4960-AAF6-A68BB9CB07E3}
EndGlobalSection
EndGlobal
22 changes: 11 additions & 11 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@
</PropertyGroup>
<ItemGroup>
<!-- ASP.NET -->
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="8.0.1" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<!-- Entity Framework -->
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.1" />
<!-- Awesome open-source packages -->
<PackageVersion Include="Ardalis.SmartEnum" Version="7.0.0" />
<PackageVersion Include="ErrorOr" Version="1.6.0" />
<PackageVersion Include="ErrorOr" Version="1.8.0" />
<PackageVersion Include="MediatR" Version="12.2.0" />
<PackageVersion Include="FluentValidation" Version="11.9.0" />
<PackageVersion Include="FluentValidation.AspNetCore" Version="11.3.0" />
Expand All @@ -27,7 +27,7 @@
<!-- Analyzers -->
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<!-- Tests -->
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.1" />
<PackageVersion Include="FluentAssertions" Version="6.12.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageVersion Include="throw" Version="1.4.0" />
Expand All @@ -36,9 +36,9 @@
<PackageVersion Include="coverlet.collector" Version="6.0.0" />
<PackageVersion Include="NSubstitute" Version="5.1.0" />
<!-- Auth -->
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="8.0.0" />
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="7.0.3" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="8.0.1" />
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="7.1.2" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.1" />
</ItemGroup>
</Project>
1 change: 1 addition & 0 deletions ErrorOr
Submodule ErrorOr added at 40de55
82 changes: 29 additions & 53 deletions src/CleanArchitecture.Api/Controllers/RemindersController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
using CleanArchitecture.Contracts.Reminders;
using CleanArchitecture.Domain.Reminders;

using ErrorOr;

using MediatR;

using Microsoft.AspNetCore.Mvc;
Expand All @@ -16,67 +18,41 @@ namespace CleanArchitecture.Api.Controllers;
public class RemindersController(ISender _mediator) : ApiController
{
[HttpPost]
public async Task<IActionResult> CreateReminder(Guid userId, Guid subscriptionId, CreateReminderRequest request)
{
var command = new SetReminderCommand(userId, subscriptionId, request.Text, request.DateTime.UtcDateTime);

var result = await _mediator.Send(command);

return result.Match(
reminder => CreatedAtAction(
actionName: nameof(GetReminder),
routeValues: new { UserId = userId, SubscriptionId = subscriptionId, ReminderId = reminder.Id },
value: ToDto(reminder)),
Problem);
}
public async Task<IActionResult> CreateReminder(Guid userId, Guid subscriptionId, CreateReminderRequest request) =>
await new SetReminderCommand(userId, subscriptionId, request.Text, request.DateTime.UtcDateTime).ToErrorOr()
.ThenAsync(command => _mediator.Send(command))
.Then(ToDto)
.Match(
reminderResponse => CreatedAtAction(
actionName: nameof(GetReminder),
routeValues: new { UserId = userId, SubscriptionId = subscriptionId, ReminderId = reminderResponse.Id },
value: reminderResponse),
Problem);

[HttpPost("{reminderId:guid}/dismiss")]
public async Task<IActionResult> DismissReminder(Guid userId, Guid subscriptionId, Guid reminderId)
{
var command = new DismissReminderCommand(userId, subscriptionId, reminderId);

var result = await _mediator.Send(command);

return result.Match(
_ => NoContent(),
Problem);
}
public async Task<IActionResult> DismissReminder(Guid userId, Guid subscriptionId, Guid reminderId) =>
await new DismissReminderCommand(userId, subscriptionId, reminderId).ToErrorOr()
.ThenAsync(command => _mediator.Send(command))
.Match(_ => NoContent(), Problem);

[HttpDelete("{reminderId:guid}")]
public async Task<IActionResult> DeleteReminder(Guid userId, Guid subscriptionId, Guid reminderId)
{
var command = new DeleteReminderCommand(userId, subscriptionId, reminderId);

var result = await _mediator.Send(command);

return result.Match(
_ => NoContent(),
Problem);
}
public async Task<IActionResult> DeleteReminder(Guid userId, Guid subscriptionId, Guid reminderId) =>
await new DeleteReminderCommand(userId, subscriptionId, reminderId).ToErrorOr()
.ThenAsync(command => _mediator.Send(command))
.Match(_ => NoContent(), Problem);

[HttpGet("{reminderId:guid}")]
public async Task<IActionResult> GetReminder(Guid userId, Guid subscriptionId, Guid reminderId)
{
var query = new GetReminderQuery(userId, subscriptionId, reminderId);

var result = await _mediator.Send(query);

return result.Match(
reminder => Ok(ToDto(reminder)),
Problem);
}
public async Task<IActionResult> GetReminder(Guid userId, Guid subscriptionId, Guid reminderId) =>
await new GetReminderQuery(userId, subscriptionId, reminderId).ToErrorOr()
.ThenAsync(query => _mediator.Send(query))
.Match(Ok, Problem);

[HttpGet]
public async Task<IActionResult> ListReminders(Guid userId, Guid subscriptionId)
{
var query = new ListRemindersQuery(userId, subscriptionId);

var result = await _mediator.Send(query);

return result.Match(
reminders => Ok(reminders.ConvertAll(ToDto)),
Problem);
}
public async Task<IActionResult> ListReminders(Guid userId, Guid subscriptionId) =>
await new ListRemindersQuery(userId, subscriptionId).ToErrorOr()
.ThenAsync(query => _mediator.Send(query))
.Then(reminders => reminders.ConvertAll(ToDto))
.Match(Ok, Problem);

private ReminderResponse ToDto(Reminder reminder) =>
new(reminder.Id, reminder.Text, reminder.DateTime, reminder.IsDismissed);
Expand Down
72 changes: 27 additions & 45 deletions src/CleanArchitecture.Api/Controllers/SubscriptionsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using CleanArchitecture.Application.Subscriptions.Queries.GetSubscription;
using CleanArchitecture.Contracts.Subscriptions;

using ErrorOr;

using MediatR;

using Microsoft.AspNetCore.Mvc;
Expand All @@ -17,55 +19,35 @@ namespace CleanArchitecture.Api.Controllers;
public class SubscriptionsController(IMediator _mediator) : ApiController
{
[HttpPost]
public async Task<IActionResult> CreateSubscription(Guid userId, CreateSubscriptionRequest request)
{
if (!DomainSubscriptionType.TryFromName(request.SubscriptionType.ToString(), out var subscriptionType))
{
return Problem(
statusCode: StatusCodes.Status400BadRequest,
detail: "Invalid plan type");
}

var command = new CreateSubscriptionCommand(
userId,
request.FirstName,
request.LastName,
request.Email,
subscriptionType);

var result = await _mediator.Send(command);

return result.Match(
subscription => CreatedAtAction(
actionName: nameof(GetSubscription),
routeValues: new { UserId = userId },
value: ToDto(subscription)),
Problem);
}
public async Task<IActionResult> CreateSubscription(Guid userId, CreateSubscriptionRequest request) =>
await DomainSubscriptionType.TryFromName(request.SubscriptionType.ToString(), out var subscriptionType).ToErrorOr()
.FailIf(val => val is false, Error.Validation("Invalid plan type"))
.Then(_ => new CreateSubscriptionCommand(
userId,
request.FirstName,
request.LastName,
request.Email,
subscriptionType))
.ThenAsync(command => _mediator.Send(command))
.Match(
subscription => CreatedAtAction(
actionName: nameof(GetSubscription),
routeValues: new { UserId = userId },
value: ToDto(subscription)),
Problem);

[HttpDelete("{subscriptionId:guid}")]
public async Task<IActionResult> DeleteSubscription(Guid userId, Guid subscriptionId)
{
var command = new CancelSubscriptionCommand(userId, subscriptionId);

var result = await _mediator.Send(command);

return result.Match(
_ => NoContent(),
Problem);
}
public async Task<IActionResult> DeleteSubscription(Guid userId, Guid subscriptionId) =>
await new CancelSubscriptionCommand(userId, subscriptionId).ToErrorOr()
.ThenAsync(command => _mediator.Send(command))
.Match(_ => NoContent(), Problem);

[HttpGet]
public async Task<IActionResult> GetSubscription(Guid userId)
{
var query = new GetSubscriptionQuery(userId);

var result = await _mediator.Send(query);

return result.Match(
user => Ok(ToDto(user)),
Problem);
}
public async Task<IActionResult> GetSubscription(Guid userId) =>
await new GetSubscriptionQuery(userId).ToErrorOr()
.ThenAsync(query => _mediator.Send(query))
.Then(ToDto)
.Match(Ok, Problem);

private static SubscriptionType ToDto(DomainSubscriptionType subscriptionType) =>
subscriptionType.Name switch
Expand Down
48 changes: 19 additions & 29 deletions src/CleanArchitecture.Api/Controllers/TokensController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
using CleanArchitecture.Contracts.Common;
using CleanArchitecture.Contracts.Tokens;

using ErrorOr;

using MediatR;

using Microsoft.AspNetCore.Authorization;
Expand All @@ -17,41 +19,29 @@ namespace CleanArchitecture.Api.Controllers;
public class TokensController(ISender _mediator) : ApiController
{
[HttpPost("generate")]
public async Task<IActionResult> GenerateToken(GenerateTokenRequest request)
{
if (!DomainSubscriptionType.TryFromName(request.SubscriptionType.ToString(), out var plan))
{
return Problem(
statusCode: StatusCodes.Status400BadRequest,
detail: "Invalid subscription type");
}

var query = new GenerateTokenQuery(
request.Id,
request.FirstName,
request.LastName,
request.Email,
plan,
request.Permissions,
request.Roles);

var result = await _mediator.Send(query);

return result.Match(
generateTokenResult => Ok(ToDto(generateTokenResult)),
Problem);
}

private static TokenResponse ToDto(GenerateTokenResult authResult)
{
return new TokenResponse(
public async Task<IActionResult> GenerateToken(GenerateTokenRequest request) =>
await DomainSubscriptionType.TryFromName(request.SubscriptionType.ToString(), out var plan).ToErrorOr()
.FailIf(val => val is false, Error.Validation("Invalid subscription type"))
.Then(_ => new GenerateTokenQuery(
request.Id,
request.FirstName,
request.LastName,
request.Email,
plan,
request.Permissions,
request.Roles))
.ThenAsync(query => _mediator.Send(query))
.Then(ToDto)
.Match(Ok, Problem);

private static ErrorOr<TokenResponse> ToDto(GenerateTokenResult authResult) =>
new TokenResponse(
authResult.Id,
authResult.FirstName,
authResult.LastName,
authResult.Email,
ToDto(authResult.SubscriptionType),
authResult.Token);
}

private static SubscriptionType ToDto(DomainSubscriptionType subscriptionType) =>
subscriptionType.Name switch
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using CleanArchitecture.Application.Common.Interfaces;
using CleanArchitecture.Domain.Reminders;
using CleanArchitecture.Domain.Users;

using ErrorOr;

Expand All @@ -12,24 +14,14 @@ public class DeleteReminderCommandHandler(
{
public async Task<ErrorOr<Success>> Handle(DeleteReminderCommand request, CancellationToken cancellationToken)
{
var reminder = await _remindersRepository.GetByIdAsync(request.ReminderId, cancellationToken);

var user = await _usersRepository.GetByIdAsync(request.UserId, cancellationToken);

if (reminder is null || user is null)
{
return Error.NotFound(description: "Reminder not found");
}

var deleteReminderResult = user.DeleteReminder(reminder);

if (deleteReminderResult.IsError)
{
return deleteReminderResult.Errors;
}

await _usersRepository.UpdateAsync(user, cancellationToken);

return Result.Success;
Reminder? reminder = await _remindersRepository.GetByIdAsync(request.ReminderId, cancellationToken);
User? user = await _usersRepository.GetByIdAsync(request.UserId, cancellationToken);

return await (Reminder: reminder, User: user).ToErrorOr()
.FailIf(pair => pair.Reminder is null || pair.User is null, Error.NotFound("Reminder not found"))
.Then(pair => (Reminder: pair.Reminder!, User: pair.User!))
.Then(pair => pair.User.DeleteReminder(pair.Reminder).Then(success => pair))
.ThenDoAsync(pair => _usersRepository.UpdateAsync(pair.User, cancellationToken))
.Then(_ => Result.Success);
}
}
Loading
Loading