Skip to content
Closed
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
3 changes: 1 addition & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,9 @@
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.9.0" />
<!--#endif-->
<PackageVersion Include="Ardalis.GuardClauses" Version="4.6.0" />
<PackageVersion Include="AutoMapper" Version="13.0.1" />
<PackageVersion Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.3.2" />
<PackageVersion Include="Azure.Identity" Version="1.13.1" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="FluentAssertions" Version="6.12.2" />
<PackageVersion Include="FluentValidation.AspNetCore" Version="11.3.0" />
<PackageVersion Include="FluentValidation.DependencyInjectionExtensions" Version="11.11.0" />
<PackageVersion Include="MediatR" Version="12.4.1" />
Expand All @@ -49,6 +47,7 @@
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="$(EfcoreVersion)" />
<!--#if (UsePostgreSQL)-->
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.1" />
<PackageVersion Include="Shouldly" Version="4.3.0" />
<PackageVersion Include="Testcontainers.PostgreSql" Version="4.0.0" />
<!--#endif-->
<!--#if (UseSqlite)-->
Expand Down
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,8 @@ azd up
* [Entity Framework Core 9](https://docs.microsoft.com/en-us/ef/core/)
* [Angular 18](https://angular.dev/) or [React 18](https://react.dev/)
* [MediatR](https://github.com/jbogard/MediatR)
* [AutoMapper](https://automapper.org/)
* [FluentValidation](https://fluentvalidation.net/)
* [NUnit](https://nunit.org/), [FluentAssertions](https://fluentassertions.com/), [Moq](https://github.com/devlooped/moq) & [Respawn](https://github.com/jbogard/Respawn)
* [NUnit](https://nunit.org/), [Shoudly](https://docs.shouldly.org/), [Moq](https://github.com/devlooped/moq) & [Respawn](https://github.com/jbogard/Respawn)

## Versions
The main branch is now on .NET 9.0. The following previous versions are available:
Expand Down
1 change: 0 additions & 1 deletion src/Application/Application.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

<ItemGroup>
<PackageReference Include="Ardalis.GuardClauses" />
<PackageReference Include="AutoMapper" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" />
<PackageReference Include="Microsoft.EntityFrameworkCore" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
Expand Down
27 changes: 23 additions & 4 deletions src/Application/Common/Mappings/MappingExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,28 @@ namespace CleanArchitecture.Application.Common.Mappings;

public static class MappingExtensions
{
public static Task<PaginatedList<TDestination>> PaginatedListAsync<TDestination>(this IQueryable<TDestination> queryable, int pageNumber, int pageSize, CancellationToken cancellationToken = default) where TDestination : class
=> PaginatedList<TDestination>.CreateAsync(queryable.AsNoTracking(), pageNumber, pageSize, cancellationToken);
public static Task<PaginatedList<TDestination>> PaginatedListAsync<TSource, TDestination>(
this IQueryable<TSource> queryable,
Func<TSource, TDestination> mapFunc,
int pageNumber,
int pageSize,
CancellationToken cancellationToken = default)
where TSource : class
where TDestination : class
{
return PaginatedList<TDestination>.CreateAsync(queryable.AsNoTracking(), mapFunc, pageNumber, pageSize, cancellationToken);
}

public static Task<List<TDestination>> ProjectToListAsync<TDestination>(this IQueryable queryable, IConfigurationProvider configuration, CancellationToken cancellationToken = default) where TDestination : class
=> queryable.ProjectTo<TDestination>(configuration).AsNoTracking().ToListAsync(cancellationToken);
public static async Task<List<TDestination>> ProjectToListAsync<TSource, TDestination>(
this IQueryable<TSource> queryable,
Func<TSource, TDestination> mapFunc,
CancellationToken cancellationToken = default)
where TSource : class
where TDestination : class
{
return await queryable
.AsNoTracking()
.Select(x => mapFunc(x))
.ToListAsync(cancellationToken);
}
}
20 changes: 13 additions & 7 deletions src/Application/Common/Models/LookupDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,19 @@ public class LookupDto
public int Id { get; init; }

public string? Title { get; init; }
}

public static class LookupDtoMapper
{
public static LookupDto FromTodoList(TodoList list) => new()
{
Id = list.Id,
Title = list.Title
};

private class Mapping : Profile
public static LookupDto FromTodoItem(TodoItem item) => new()
{
public Mapping()
{
CreateMap<TodoList, LookupDto>();
CreateMap<TodoItem, LookupDto>();
}
}
Id = item.Id,
Title = item.Title
};
}
11 changes: 8 additions & 3 deletions src/Application/Common/Models/PaginatedList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,16 @@ public PaginatedList(IReadOnlyCollection<T> items, int count, int pageNumber, in

public bool HasNextPage => PageNumber < TotalPages;

public static async Task<PaginatedList<T>> CreateAsync(IQueryable<T> source, int pageNumber, int pageSize, CancellationToken cancellationToken = default)
public static async Task<PaginatedList<TDestination>> CreateAsync<TSource, TDestination>(
IQueryable<TSource> source,
Func<TSource, TDestination> mapFunc,
int pageNumber,
int pageSize,
CancellationToken cancellationToken = default)
{
var count = await source.CountAsync(cancellationToken);
var items = await source.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToListAsync(cancellationToken);

return new PaginatedList<T>(items, count, pageNumber, pageSize);
var mappedItems = items.Select(mapFunc).ToList();
return new PaginatedList<TDestination>(mappedItems, count, pageNumber, pageSize);
}
}
2 changes: 0 additions & 2 deletions src/Application/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ public static class DependencyInjection
{
public static void AddApplicationServices(this IHostApplicationBuilder builder)
{
builder.Services.AddAutoMapper(Assembly.GetExecutingAssembly());

builder.Services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());

builder.Services.AddMediatR(cfg => {
Expand Down
4 changes: 1 addition & 3 deletions src/Application/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
global using Ardalis.GuardClauses;
global using AutoMapper;
global using AutoMapper.QueryableExtensions;
global using Microsoft.EntityFrameworkCore;
global using FluentValidation;
global using MediatR;
global using MediatR;
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,17 @@ public record GetTodoItemsWithPaginationQuery : IRequest<PaginatedList<TodoItemB
public class GetTodoItemsWithPaginationQueryHandler : IRequestHandler<GetTodoItemsWithPaginationQuery, PaginatedList<TodoItemBriefDto>>
{
private readonly IApplicationDbContext _context;
private readonly IMapper _mapper;

public GetTodoItemsWithPaginationQueryHandler(IApplicationDbContext context, IMapper mapper)
public GetTodoItemsWithPaginationQueryHandler(IApplicationDbContext context)
{
_context = context;
_mapper = mapper;
}

public async Task<PaginatedList<TodoItemBriefDto>> Handle(GetTodoItemsWithPaginationQuery request, CancellationToken cancellationToken)
{
return await _context.TodoItems
.Where(x => x.ListId == request.ListId)
.OrderBy(x => x.Title)
.ProjectTo<TodoItemBriefDto>(_mapper.ConfigurationProvider)
.PaginatedListAsync(request.PageNumber, request.PageSize, cancellationToken);
.Where(x => x.ListId == request.ListId)
.OrderBy(x => x.Title)
.PaginatedListAsync(TodoItemBriefDtoMapper.FromEntity, request.PageNumber, request.PageSize, cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ public class TodoItemBriefDto
public string? Title { get; init; }

public bool Done { get; init; }
}

private class Mapping : Profile
public static class TodoItemBriefDtoMapper
{
public static TodoItemBriefDto FromEntity(TodoItem item) => new()
{
public Mapping()
{
CreateMap<TodoItem, TodoItemBriefDto>();
}
}
Id = item.Id,
ListId = item.ListId,
Title = item.Title,
Done = item.Done
};
}
17 changes: 9 additions & 8 deletions src/Application/TodoLists/Queries/GetTodos/GetTodos.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using CleanArchitecture.Application.Common.Interfaces;
using CleanArchitecture.Application.Common.Mappings;
using CleanArchitecture.Application.Common.Models;
using CleanArchitecture.Application.Common.Security;
using CleanArchitecture.Domain.Enums;
Expand All @@ -11,28 +12,28 @@ public record GetTodosQuery : IRequest<TodosVm>;
public class GetTodosQueryHandler : IRequestHandler<GetTodosQuery, TodosVm>
{
private readonly IApplicationDbContext _context;
private readonly IMapper _mapper;

public GetTodosQueryHandler(IApplicationDbContext context, IMapper mapper)
public GetTodosQueryHandler(IApplicationDbContext context)
{
_context = context;
_mapper = mapper;
}

public async Task<TodosVm> Handle(GetTodosQuery request, CancellationToken cancellationToken)
{
var lists = await _context.TodoLists
.Include(l => l.Items)
.OrderBy(t => t.Title)
.ProjectToListAsync(TodoListDtoMapper.FromEntity, cancellationToken);

return new TodosVm
{
PriorityLevels = Enum.GetValues(typeof(PriorityLevel))
.Cast<PriorityLevel>()
.Select(p => new LookupDto { Id = (int)p, Title = p.ToString() })
.ToList(),

Lists = await _context.TodoLists
.AsNoTracking()
.ProjectTo<TodoListDto>(_mapper.ConfigurationProvider)
.OrderBy(t => t.Title)
.ToListAsync(cancellationToken)
Lists = lists
};
}
}

19 changes: 12 additions & 7 deletions src/Application/TodoLists/Queries/GetTodos/TodoItemDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,18 @@ public class TodoItemDto
public int Priority { get; init; }

public string? Note { get; init; }
}


private class Mapping : Profile
public static class TodoItemDtoMapper
{
public static TodoItemDto FromEntity(TodoItem item) => new()
{
public Mapping()
{
CreateMap<TodoItem, TodoItemDto>().ForMember(d => d.Priority,
opt => opt.MapFrom(s => (int)s.Priority));
}
}
Id = item.Id,
ListId = item.ListId,
Title = item.Title,
Done = item.Done,
Priority = (int)item.Priority,
Note = item.Note
};
}
18 changes: 12 additions & 6 deletions src/Application/TodoLists/Queries/GetTodos/TodoListDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,18 @@ public TodoListDto()
public string? Colour { get; init; }

public IReadOnlyCollection<TodoItemDto> Items { get; init; }
}

private class Mapping : Profile
public static class TodoListDtoMapper
{
public static TodoListDto FromEntity(TodoList list) => new()
{
public Mapping()
{
CreateMap<TodoList, TodoListDto>();
}
}
Id = list.Id,
Title = list.Title,
Colour = list.Colour,
Items = list.Items?
.Select(TodoItemDtoMapper.FromEntity)
.ToList()
?? new List<TodoItemDto>()
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Moq" />
<PackageReference Include="Respawn" />
<PackageReference Include="Shouldly" />
<PackageReference Include="System.Configuration.ConfigurationManager" />
<!--#if (UsePostgreSQL)-->
<PackageReference Include="Testcontainers.PostgreSql" />
Expand Down
4 changes: 2 additions & 2 deletions tests/Application.FunctionalTests/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
global using Ardalis.GuardClauses;
global using FluentAssertions;
global using Shouldly;
global using Moq;
global using NUnit.Framework;
global using NUnit.Framework;
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ public async Task ShouldRequireMinimumFields()
{
var command = new CreateTodoItemCommand();

await FluentActions.Invoking(() =>
SendAsync(command)).Should().ThrowAsync<ValidationException>();
await Should.ThrowAsync<ValidationException>(() => SendAsync(command));
}

[Test]
Expand All @@ -38,12 +37,12 @@ public async Task ShouldCreateTodoItem()

var item = await FindAsync<TodoItem>(itemId);

item.Should().NotBeNull();
item!.ListId.Should().Be(command.ListId);
item.Title.Should().Be(command.Title);
item.CreatedBy.Should().Be(userId);
item.Created.Should().BeCloseTo(DateTime.Now, TimeSpan.FromMilliseconds(10000));
item.LastModifiedBy.Should().Be(userId);
item.LastModified.Should().BeCloseTo(DateTime.Now, TimeSpan.FromMilliseconds(10000));
item.ShouldNotBeNull();
item!.ListId.ShouldBe(command.ListId);
item.Title.ShouldBe(command.Title);
item.CreatedBy.ShouldBe(userId);
item.Created.ShouldBe(DateTime.Now, TimeSpan.FromMilliseconds(10000));
item.LastModifiedBy.ShouldBe(userId);
item.LastModified.ShouldBe(DateTime.Now, TimeSpan.FromMilliseconds(10000));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ public async Task ShouldRequireValidTodoItemId()
{
var command = new DeleteTodoItemCommand(99);

await FluentActions.Invoking(() =>
SendAsync(command)).Should().ThrowAsync<NotFoundException>();
await Should.ThrowAsync<NotFoundException>(() => SendAsync(command));
}

[Test]
Expand All @@ -36,6 +35,6 @@ public async Task ShouldDeleteTodoItem()

var item = await FindAsync<TodoItem>(itemId);

item.Should().BeNull();
item.ShouldBeNull();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ public class UpdateTodoItemDetailTests : BaseTestFixture
public async Task ShouldRequireValidTodoItemId()
{
var command = new UpdateTodoItemCommand { Id = 99, Title = "New Title" };
await FluentActions.Invoking(() => SendAsync(command)).Should().ThrowAsync<NotFoundException>();

await Should.ThrowAsync<NotFoundException>(() => SendAsync(command));
}

[Test]
Expand Down Expand Up @@ -46,12 +47,12 @@ public async Task ShouldUpdateTodoItem()

var item = await FindAsync<TodoItem>(itemId);

item.Should().NotBeNull();
item!.ListId.Should().Be(command.ListId);
item.Note.Should().Be(command.Note);
item.Priority.Should().Be(command.Priority);
item.LastModifiedBy.Should().NotBeNull();
item.LastModifiedBy.Should().Be(userId);
item.LastModified.Should().BeCloseTo(DateTime.Now, TimeSpan.FromMilliseconds(10000));
item.ShouldNotBeNull();
item!.ListId.ShouldBe(command.ListId);
item.Note.ShouldBe(command.Note);
item.Priority.ShouldBe(command.Priority);
item.LastModifiedBy.ShouldNotBeNull();
item.LastModifiedBy.ShouldBe(userId);
item.LastModified.ShouldBe(DateTime.Now, TimeSpan.FromMilliseconds(10000));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class UpdateTodoItemTests : BaseTestFixture
public async Task ShouldRequireValidTodoItemId()
{
var command = new UpdateTodoItemCommand { Id = 99, Title = "New Title" };
await FluentActions.Invoking(() => SendAsync(command)).Should().ThrowAsync<NotFoundException>();
await Should.ThrowAsync<NotFoundException>(() => SendAsync(command));
}

[Test]
Expand Down Expand Up @@ -42,10 +42,10 @@ public async Task ShouldUpdateTodoItem()

var item = await FindAsync<TodoItem>(itemId);

item.Should().NotBeNull();
item!.Title.Should().Be(command.Title);
item.LastModifiedBy.Should().NotBeNull();
item.LastModifiedBy.Should().Be(userId);
item.LastModified.Should().BeCloseTo(DateTime.Now, TimeSpan.FromMilliseconds(10000));
item.ShouldNotBeNull();
item!.Title.ShouldBe(command.Title);
item.LastModifiedBy.ShouldNotBeNull();
item.LastModifiedBy.ShouldBe(userId);
item.LastModified.ShouldBe(DateTime.Now, TimeSpan.FromMilliseconds(10000));
}
}
Loading
Loading