Skip to content

Commit

Permalink
feat: Architecture refinement (#16)
Browse files Browse the repository at this point in the history
* refactor: Restructured solution architecture.
* chore: Updated README.md
* refactor: Removed old projects.
  • Loading branch information
philipp-meier authored Aug 4, 2023
1 parent 3968fba commit 90035a2
Show file tree
Hide file tree
Showing 97 changed files with 22,100 additions and 17,904 deletions.
20 changes: 3 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
</p>

---

[![CI](https://github.com/philipp-meier/Chrono/actions/workflows/dotnet.yml/badge.svg)](https://github.com/philipp-meier/Chrono/actions/workflows/dotnet.yml)
[![CodeScene Code Health](https://codescene.io/projects/41477/status-badges/code-health)](https://codescene.io/projects)
[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://github.com/philipp-meier/Chrono/blob/main/LICENSE)
Expand Down Expand Up @@ -63,27 +64,12 @@ Once that is done, you can run the application with (for example) `dotnet watch`

```sh
sqlite3 chrono.db "VACUUM;"
dotnet ef migrations add Initial --project Infrastructure.csproj --startup-project ../WebUI/WebUI.csproj
dotnet ef database update --project Infrastructure.csproj --startup-project ../WebUI/WebUI.csproj
dotnet ef migrations add Initial --project ../Application/Application.csproj --startup-project WebUI.csproj
dotnet ef database update --project ../Application/Application.csproj --startup-project WebUI.csproj
```

## Technology

- **Backend**: ASP.NET Core Web API
- **Frontend**: React with Semantic UI
- **Tools / Extensions**: Editorconfig, Prettier

## Software Architecture

The architecture of this project mostly follows the [Clean Architecture Solution Template for ASP.NET Core](https://github.com/jasontaylordev/CleanArchitecture) by Jason Taylor.

| Project | Description |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Domain | Will contain all entities, enums, exceptions, interfaces, types and logic specific to the domain layer. |
| Application | Contains all application logic. It is dependent on the domain layer, but has no dependencies on any other layer or project. This layer defines interfaces that are implemented outside layers. For example, if the application need to access a notification service, a new interface would be added to application and an implementation would be created within infrastructure. |
| Infrastructure | This layer contains classes for accessing external resources such as file systems, web services, smtp and so on. These classes should be based on interfaces defined within the application layer. |
| WebUI | This layer is a single page application based on React and ASP.NET Core. It depends on both the Application and Infrastructure layers, however, the dependency on Infrastructure is only to support dependency injection. Therefore only Startup.cs should reference Infrastructure. |

## Why?

A personal goal for this project is to get myself familiar and comfortable with the clean architecture template by Jason Taylor and see how it affects projects in the long-run. Besides that, I am also trying to learn React with this project to compare it to other frameworks I have already used before.
25 changes: 22 additions & 3 deletions src/Application/Application.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,30 @@
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Domain\Domain.csproj" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.6.0"/>
<PackageReference Include="MediatR" Version="12.1.1"/>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.9"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.9">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.9"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.9">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.6.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.9" />
<Reference Include="Microsoft.EntityFrameworkCore.Sqlite">
<HintPath>..\..\..\..\.nuget\packages\microsoft.entityframeworkcore.sqlite.core\7.0.9\lib\net6.0\Microsoft.EntityFrameworkCore.Sqlite.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Extensions.Configuration.Abstractions">
<HintPath>..\..\..\..\.nuget\packages\microsoft.extensions.configuration.abstractions\7.0.0\lib\net7.0\Microsoft.Extensions.Configuration.Abstractions.dll</HintPath>
</Reference>
</ItemGroup>

<ItemGroup>
<Folder Include="Features\"/>
</ItemGroup>
</Project>

This file was deleted.

12 changes: 9 additions & 3 deletions src/Application/Common/Dtos/CategoryDto.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Chrono.Domain.Entities;
using Chrono.Application.Entities;

namespace Chrono.Application.Common.Dtos;

Expand All @@ -7,7 +7,13 @@ public class CategoryDto
public int Id { get; init; }
public string Name { get; init; }

public static CategoryDto FromEntity(Category category) => new() { Id = category.Id, Name = category.Name };
public static CategoryDto FromEntity(Category category)
{
return new CategoryDto { Id = category.Id, Name = category.Name };
}

public static CategoryDto FromEntity(TaskCategory taskCategory) => new() { Id = taskCategory.Category.Id, Name = taskCategory.Category.Name };
public static CategoryDto FromEntity(TaskCategory taskCategory)
{
return new CategoryDto { Id = taskCategory.Category.Id, Name = taskCategory.Category.Name };
}
}
3 changes: 1 addition & 2 deletions src/Application/Common/Dtos/TaskDto.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Task = Chrono.Domain.Entities.Task;
using Task = Chrono.Application.Entities.Task;

namespace Chrono.Application.Common.Dtos;

Expand Down Expand Up @@ -30,7 +30,6 @@ public static TaskDto FromEntity(Task task)
Categories = task.Categories
.Select(CategoryDto.FromEntity)
.ToArray(),

LastModified = DateTime.SpecifyKind(task.LastModified, DateTimeKind.Utc),
LastModifiedBy = task.LastModifiedBy.Name
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
using Chrono.Domain.Common;
using Chrono.Application.Entities.Common;

namespace Chrono.Application.Common.Security;
namespace Chrono.Application.Common.Extensions;

public static class SecurityExtensions
{
public static bool IsPermitted(this BaseAuditableEntity context, string userId)
{
if (string.IsNullOrEmpty(userId))
{
return false;
}

// Only the owner is permitted to view/edit the object.
return context.CreatedBy.UserId == userId;
Expand Down
4 changes: 2 additions & 2 deletions src/Application/Common/Interfaces/IApplicationDbContext.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Chrono.Domain.Entities;
using Chrono.Application.Entities;
using Microsoft.EntityFrameworkCore;
using Task = Chrono.Domain.Entities.Task;
using Task = Chrono.Application.Entities.Task;

namespace Chrono.Application.Common.Interfaces;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
using Task = Chrono.Domain.Entities.Task;
using Chrono.Application.Entities;
using Task = Chrono.Application.Entities.Task;

namespace Chrono.Domain.Services;
namespace Chrono.Application.Common.Services;

public static class TaskListService
{
public static void ReorderTaskPositions(TaskList taskList)
{
if (taskList == null)
{
throw new ArgumentNullException(nameof(taskList));
}

var allTasks = taskList.Tasks
.OrderBy(x => x.Position)
Expand All @@ -20,15 +23,19 @@ void EnsureTaskPositionsWithinGroup(IEnumerable<Task> tasks, Func<Task, bool> ta
{
var filteredTasks = tasks.Where(taskGroupFilter).ToArray();
for (var i = 0; i < filteredTasks.Length; i++)
{
filteredTasks[i].Position = i + 1;
}
}
}

public static void InsertAt(int position, Task task, TaskList targetTaskList = null)
{
var taskList = targetTaskList ?? task?.List;
if (taskList == null)
{
throw new ArgumentNullException(nameof(taskList));
}

if (targetTaskList != null && task.List != targetTaskList)
{
Expand All @@ -43,21 +50,29 @@ public static void InsertAt(int position, Task task, TaskList targetTaskList = n

var newPosition = position;
if (newPosition > tasks.Length)
{
newPosition = tasks.Length;
}
else if (newPosition < 1)
{
newPosition = 1;
}

var positionCount = 0;
foreach (var currentTask in tasks)
{
if (currentTask == task)
{
continue;
}

positionCount++;

// Skip this position.
if (positionCount == newPosition)
{
positionCount++;
}

currentTask.Position = positionCount;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Task = Chrono.Domain.Entities.Task;
using Chrono.Application.Entities;
using Task = Chrono.Application.Entities.Task;

namespace Chrono.Domain.Services;
namespace Chrono.Application.Common.Services;

public static class TaskService
{
Expand All @@ -13,15 +14,23 @@ public static void SetCategories(Task context, Category[] newCategories)
foreach (var existing in currentCategories)
{
if (newCategories.Contains(existing))
{
continue;
}

var assignmentsToRemove = context.Categories.Where(x => x.Category == existing).ToArray();
foreach (var toRemove in assignmentsToRemove)
{
context.Categories.Remove(toRemove);
}
}

foreach (var newCategory in newCategories)
{
if (!currentCategories.Contains(newCategory))
{
context.Categories.Add(new TaskCategory { Task = context, Category = newCategory });
}
}
}
}
8 changes: 5 additions & 3 deletions src/Application/ConfigureServices.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
using MediatR;
using FluentValidation;
using System.Reflection;
using Chrono.Application.Common.Behaviors;
using FluentValidation;
using MediatR;

namespace Microsoft.Extensions.DependencyInjection;

public static class ConfigureServices
public static partial class ConfigureServices
{
public static void AddApplicationServices(this IServiceCollection services, bool isDevelopment)
{
Expand All @@ -16,7 +16,9 @@ public static void AddApplicationServices(this IServiceCollection services, bool
cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));

if (isDevelopment)
{
cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(PerformanceBehavior<,>));
}
});
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace Chrono.Domain.Entities;
using Chrono.Application.Entities.Common;

namespace Chrono.Application.Entities;

public class Category : BaseAuditableEntity
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Chrono.Domain.Common;
namespace Chrono.Application.Entities.Common;

public abstract class BaseAuditableEntity : BaseEntity
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Chrono.Domain.Common;
namespace Chrono.Application.Entities.Common;

public abstract class BaseEntity
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace Chrono.Domain.Entities;
using Chrono.Application.Entities.Common;

namespace Chrono.Application.Entities;

public class Task : BaseAuditableEntity
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Chrono.Domain.Entities;
namespace Chrono.Application.Entities;

public class TaskCategory
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace Chrono.Domain.Entities;
using Chrono.Application.Entities.Common;

namespace Chrono.Application.Entities;

public class TaskList : BaseAuditableEntity
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Chrono.Domain.Entities;
namespace Chrono.Application.Entities;

public class TaskListOptions
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace Chrono.Domain.Entities;
using Chrono.Application.Entities.Common;

namespace Chrono.Application.Entities;

public class User : BaseEntity
{
Expand Down
27 changes: 27 additions & 0 deletions src/Application/Features/Categories/CreateCategory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Chrono.Application.Common.Interfaces;
using Chrono.Application.Entities;
using MediatR;

namespace Chrono.Application.Features.Categories;

public record CreateCategory(string Name) : IRequest<int>;

public class CreateCategoryHandler : IRequestHandler<CreateCategory, int>
{
private readonly IApplicationDbContext _context;

public CreateCategoryHandler(IApplicationDbContext context)
{
_context = context;
}

public async Task<int> Handle(CreateCategory request, CancellationToken cancellationToken)
{
var entity = new Category { Name = request.Name };
_context.Categories.Add(entity);

await _context.SaveChangesAsync(cancellationToken);

return entity.Id;
}
}
Loading

0 comments on commit 90035a2

Please sign in to comment.