├── _config.yml ├── src ├── CleanCli.Todo.Console │ ├── .gitignore │ ├── Properties │ │ └── launchSettings.json │ ├── Commands │ │ ├── MigrateCommand.cs │ │ ├── SeedTodoItemsCommand.cs │ │ ├── DeleteTodoListCommand.cs │ │ ├── CreateTodoListCommand.cs │ │ ├── GetTodoListCommand.cs │ │ └── ListTodosCommand.cs │ ├── appsettings.json │ ├── CleanCli.Todo.Console.csproj │ ├── Program.cs │ └── ServiceCollectionExtensions.cs ├── Application │ ├── Common │ │ ├── Interfaces │ │ │ ├── ICurrentUserService.cs │ │ │ ├── IDateTime.cs │ │ │ ├── IDomainEventService.cs │ │ │ └── IApplicationDbContext.cs │ │ ├── Mappings │ │ │ ├── IMapFrom.cs │ │ │ ├── MappingExtensions.cs │ │ │ └── MappingProfile.cs │ │ ├── Models │ │ │ ├── DomainEventNotification.cs │ │ │ ├── Result.cs │ │ │ └── PaginatedList.cs │ │ ├── Exceptions │ │ │ ├── NotFoundException.cs │ │ │ └── ValidationException.cs │ │ └── Behaviours │ │ │ ├── UnhandledExceptionBehaviour.cs │ │ │ ├── LoggingBehaviour.cs │ │ │ ├── ValidationBehaviour.cs │ │ │ └── PerformanceBehaviour.cs │ ├── TodoLists │ │ ├── Queries │ │ │ ├── GetTodos │ │ │ │ ├── PriorityLevelDto.cs │ │ │ │ ├── TodosVm.cs │ │ │ │ ├── TodoListDto.cs │ │ │ │ ├── TodoItemDto.cs │ │ │ │ └── GetTodosQuery.cs │ │ │ └── GetTodo │ │ │ │ └── GetTodoQuery.cs │ │ └── Commands │ │ │ ├── PurgeTodoLists │ │ │ └── PurgeTodoListsCommand.cs │ │ │ ├── CreateTodoList │ │ │ ├── CreateTodoListCommand.cs │ │ │ └── CreateTodoListCommandValidator.cs │ │ │ ├── UpdateTodoList │ │ │ ├── UpdateTodoListCommandValidator.cs │ │ │ └── UpdateTodoListCommand.cs │ │ │ └── DeleteTodoList │ │ │ └── DeleteTodoListCommand.cs │ ├── TodoItems │ │ ├── Commands │ │ │ ├── CreateTodoItem │ │ │ │ ├── CreateTodoItemCommandValidator.cs │ │ │ │ └── CreateTodoItemCommand.cs │ │ │ ├── UpdateTodoItem │ │ │ │ ├── UpdateTodoItemCommandValidator.cs │ │ │ │ └── UpdateTodoItemCommand.cs │ │ │ ├── DeleteTodoItem │ │ │ │ └── DeleteTodoItemCommand.cs │ │ │ └── UpdateTodoItemDetail │ │ │ │ └── UpdateTodoItemDetailCommand.cs │ │ ├── Queries │ │ │ └── GetTodoItemsWithPagination │ │ │ │ ├── GetTodoItemsWithPaginationQueryValidator.cs │ │ │ │ └── GetTodoItemsWithPaginationQuery.cs │ │ └── EventHandlers │ │ │ ├── TodoItemCreatedHandler.cs │ │ │ └── TodoItemCompletedHandler.cs │ ├── Application.csproj │ └── ServiceCollectionExtensions.cs ├── Domain │ ├── Enums │ │ └── PriorityLevel.cs │ ├── Domain.csproj │ ├── Exceptions │ │ └── UnsupportedColourException.cs │ ├── Events │ │ ├── TodoItemCreatedEvent.cs │ │ └── TodoItemCompletedEvent.cs │ ├── Common │ │ ├── AuditableEntity.cs │ │ ├── DomainEvent.cs │ │ └── ValueObject.cs │ ├── Entities │ │ ├── TodoList.cs │ │ └── TodoItem.cs │ └── ValueObjects │ │ └── Colour.cs └── Infrastructure │ ├── Services │ ├── DateTimeService.cs │ └── DomainEventService.cs │ ├── Persistence │ ├── Configurations │ │ ├── TodoItemConfiguration.cs │ │ └── TodoListConfiguration.cs │ ├── ApplicationDbContextSeed.cs │ └── ApplicationDbContext.cs │ ├── ServiceCollectionExtensions.cs │ └── Infrastructure.csproj ├── global.json ├── .github ├── ISSUE_TEMPLATE │ ├── QUESTION.md │ ├── FEATURE_REQUEST.md │ └── BUG_REPORT.md └── workflows │ └── build.yml ├── tests ├── .editorconfig └── Directory.Build.props ├── .config └── dotnet-tools.json ├── Directory.Build.props ├── .gitattributes ├── CleanCli.Todo.sln ├── README.md ├── .gitignore └── .editorconfig /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-midnight -------------------------------------------------------------------------------- /src/CleanCli.Todo.Console/.gitignore: -------------------------------------------------------------------------------- 1 | todo.db 2 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "5.0.200", 4 | "rollForward": "latestMinor" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/CleanCli.Todo.Console/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "CleanCli.Todo.Console": { 4 | "commandName": "Project", 5 | "commandLineArgs": "todolist get 1" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Application/Common/Interfaces/ICurrentUserService.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.Common.Interfaces 2 | { 3 | public interface ICurrentUserService 4 | { 5 | string UserId { get; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Application/Common/Interfaces/IDateTime.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.Common.Interfaces 2 | { 3 | using System; 4 | 5 | public interface IDateTime 6 | { 7 | DateTime Now { get; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Domain/Enums/PriorityLevel.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Domain.Enums 2 | { 3 | public enum PriorityLevel 4 | { 5 | None = 0, 6 | Low = 1, 7 | Medium = 2, 8 | High = 3 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/QUESTION.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a general question about how to use the project 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | --- 8 | 9 | ### Question 10 | 11 | Ask your question. 12 | -------------------------------------------------------------------------------- /tests/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # VSTHRD200: Use "Async" suffix for async methods 4 | dotnet_diagnostic.VSTHRD200.severity = silent 5 | 6 | # CA1707: Identifiers should not contain underscores 7 | dotnet_diagnostic.CA1707.severity = none 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | ### Describe the feature 10 | 11 | A clear and concise description of what the feature is. 12 | -------------------------------------------------------------------------------- /src/Application/Common/Mappings/IMapFrom.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.Common.Mappings 2 | { 3 | using AutoMapper; 4 | 5 | public interface IMapFrom 6 | { 7 | void Mapping(Profile profile) => profile.CreateMap(typeof(T), this.GetType()); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Application/TodoLists/Queries/GetTodos/PriorityLevelDto.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.TodoLists.Queries.GetTodos 2 | { 3 | public class PriorityLevelDto 4 | { 5 | public int Value { get; set; } 6 | 7 | public string Name { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Domain/Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | CleanCli.Todo.Domain 6 | CleanCli.Todo.Domain 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Infrastructure/Services/DateTimeService.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Infrastructure.Services 2 | { 3 | using CleanCli.Todo.Application.Common.Interfaces; 4 | using System; 5 | 6 | public class DateTimeService : IDateTime 7 | { 8 | public DateTime Now => DateTime.Now; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Application/Common/Interfaces/IDomainEventService.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.Common.Interfaces 2 | { 3 | using CleanCli.Todo.Domain.Common; 4 | using System.Threading.Tasks; 5 | 6 | public interface IDomainEventService 7 | { 8 | Task Publish(DomainEvent domainEvent); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "cake.tool": { 6 | "version": "1.1.0", 7 | "commands": [ 8 | "dotnet-cake" 9 | ] 10 | }, 11 | "dotnet-ef": { 12 | "version": "5.0.6", 13 | "commands": [ 14 | "dotnet-ef" 15 | ] 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/Application/TodoLists/Queries/GetTodos/TodosVm.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.TodoLists.Queries.GetTodos 2 | { 3 | using System.Collections.Generic; 4 | 5 | public class TodosVm 6 | { 7 | public IList PriorityLevels { get; set; } 8 | 9 | public IList Lists { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Domain/Exceptions/UnsupportedColourException.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Domain.Exceptions 2 | { 3 | using System; 4 | 5 | public class UnsupportedColourException : Exception 6 | { 7 | public UnsupportedColourException(string code) 8 | : base($"Colour \"{code}\" is unsupported.") 9 | { 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Domain/Events/TodoItemCreatedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Domain.Events 2 | { 3 | using CleanCli.Todo.Domain.Common; 4 | using CleanCli.Todo.Domain.Entities; 5 | 6 | public class TodoItemCreatedEvent : DomainEvent 7 | { 8 | public TodoItemCreatedEvent(TodoItem item) => this.Item = item; 9 | 10 | public TodoItem Item { get; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Domain/Events/TodoItemCompletedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Domain.Events 2 | { 3 | using CleanCli.Todo.Domain.Common; 4 | using CleanCli.Todo.Domain.Entities; 5 | 6 | public class TodoItemCompletedEvent : DomainEvent 7 | { 8 | public TodoItemCompletedEvent(TodoItem item) => this.Item = item; 9 | 10 | public TodoItem Item { get; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Domain/Common/AuditableEntity.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Domain.Common 2 | { 3 | using System; 4 | 5 | public abstract class AuditableEntity 6 | { 7 | public DateTime Created { get; set; } 8 | 9 | public string CreatedBy { get; set; } 10 | 11 | public DateTime? LastModified { get; set; } 12 | 13 | public string LastModifiedBy { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Application/Common/Models/DomainEventNotification.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.Common.Models 2 | { 3 | using CleanCli.Todo.Domain.Common; 4 | using MediatR; 5 | 6 | public class DomainEventNotification : INotification where TDomainEvent : DomainEvent 7 | { 8 | public DomainEventNotification(TDomainEvent domainEvent) => this.DomainEvent = domainEvent; 9 | 10 | public TDomainEvent DomainEvent { get; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Application/TodoItems/Commands/CreateTodoItem/CreateTodoItemCommandValidator.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.TodoItems.Commands.CreateTodoItem 2 | { 3 | using FluentValidation; 4 | 5 | public class CreateTodoItemCommandValidator : AbstractValidator 6 | { 7 | public CreateTodoItemCommandValidator() => 8 | this.RuleFor(v => v.Title) 9 | .MaximumLength(200) 10 | .NotEmpty(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Application/TodoItems/Commands/UpdateTodoItem/UpdateTodoItemCommandValidator.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.TodoItems.Commands.UpdateTodoItem 2 | { 3 | using FluentValidation; 4 | 5 | public class UpdateTodoItemCommandValidator : AbstractValidator 6 | { 7 | public UpdateTodoItemCommandValidator() => 8 | this.RuleFor(v => v.Title) 9 | .MaximumLength(200) 10 | .NotEmpty(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Domain/Entities/TodoList.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Domain.Entities 2 | { 3 | using CleanCli.Todo.Domain.Common; 4 | using CleanCli.Todo.Domain.ValueObjects; 5 | using System.Collections.Generic; 6 | 7 | public class TodoList : AuditableEntity 8 | { 9 | public int Id { get; set; } 10 | 11 | public string Title { get; set; } 12 | 13 | public Colour Colour { get; set; } = Colour.White; 14 | 15 | public IList Items { get; private set; } = new List(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Application/Common/Interfaces/IApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.Common.Interfaces 2 | { 3 | using CleanCli.Todo.Domain.Entities; 4 | using Microsoft.EntityFrameworkCore; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | public interface IApplicationDbContext 9 | { 10 | DbSet TodoLists { get; set; } 11 | 12 | DbSet TodoItems { get; set; } 13 | 14 | Task SaveChangesAsync(CancellationToken cancellationToken); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Domain/Common/DomainEvent.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Domain.Common 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | public interface IHasDomainEvent 7 | { 8 | public List DomainEvents { get; set; } 9 | } 10 | 11 | public abstract class DomainEvent 12 | { 13 | protected DomainEvent() => this.DateOccurred = DateTimeOffset.UtcNow; 14 | public bool IsPublished { get; set; } 15 | public DateTimeOffset DateOccurred { get; protected set; } = DateTime.UtcNow; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | ### Describe the bug 10 | 11 | A clear and concise description of what the bug is. 12 | 13 | ### To Reproduce 14 | 15 | A link to some code to reproduce the bug can speed up a fix. Alternatively, show steps to reproduce the behaviour: 16 | 17 | 1. Go to '...' 18 | 2. Click on '....' 19 | 3. Scroll down to '....' 20 | 4. See error 21 | 22 | ### Expected behaviour 23 | 24 | A clear and concise description of what you expected to happen. 25 | -------------------------------------------------------------------------------- /src/Application/TodoLists/Queries/GetTodos/TodoListDto.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.TodoLists.Queries.GetTodos 2 | { 3 | using CleanCli.Todo.Application.Common.Mappings; 4 | using CleanCli.Todo.Domain.Entities; 5 | using System.Collections.Generic; 6 | 7 | public class TodoListDto : IMapFrom 8 | { 9 | public TodoListDto() => this.Items = new List(); 10 | 11 | public int Id { get; set; } 12 | 13 | public string Title { get; set; } 14 | 15 | public string Colour { get; set; } 16 | 17 | public IList Items { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Infrastructure/Persistence/Configurations/TodoItemConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Infrastructure.Persistence.Configurations 2 | { 3 | using CleanCli.Todo.Domain.Entities; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 6 | 7 | public class TodoItemConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.Ignore(e => e.DomainEvents); 12 | 13 | builder.Property(t => t.Title) 14 | .HasMaxLength(200) 15 | .IsRequired(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Infrastructure/Persistence/Configurations/TodoListConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Infrastructure.Persistence.Configurations 2 | { 3 | using CleanCli.Todo.Domain.Entities; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 6 | 7 | public class TodoListConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.Property(t => t.Title) 12 | .HasMaxLength(200) 13 | .IsRequired(); 14 | 15 | builder 16 | .OwnsOne(b => b.Colour); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Application/Common/Exceptions/NotFoundException.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.Common.Exceptions 2 | { 3 | using System; 4 | 5 | public class NotFoundException : Exception 6 | { 7 | public NotFoundException() 8 | : base() 9 | { 10 | } 11 | 12 | public NotFoundException(string message) 13 | : base(message) 14 | { 15 | } 16 | 17 | public NotFoundException(string message, Exception innerException) 18 | : base(message, innerException) 19 | { 20 | } 21 | 22 | public NotFoundException(string name, object key) 23 | : base($"Entity \"{name}\" ({key}) was not found.") 24 | { 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Application/Common/Models/Result.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.Common.Models 2 | { 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | public class Result 7 | { 8 | internal Result(bool succeeded, IEnumerable errors) 9 | { 10 | this.Succeeded = succeeded; 11 | this.Errors = errors.ToArray(); 12 | } 13 | 14 | public bool Succeeded { get; set; } 15 | 16 | public string[] Errors { get; set; } 17 | 18 | public static Result Success() => 19 | new Result(true, System.Array.Empty()); 20 | 21 | public static Result Failure(IEnumerable errors) => 22 | new Result(false, errors); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9.0 4 | latest 5 | 6 | 7 | 8 | 9 | 10 | 11 | nikiforovall 12 | nikiforovall 13 | 14 | 15 | 16 | https://github.com/NikiforovAll/clean-cli-todo-example 17 | 18 | en-US 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Application/TodoLists/Queries/GetTodos/TodoItemDto.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.TodoLists.Queries.GetTodos 2 | { 3 | using AutoMapper; 4 | using CleanCli.Todo.Application.Common.Mappings; 5 | using CleanCli.Todo.Domain.Entities; 6 | 7 | public class TodoItemDto : IMapFrom 8 | { 9 | public int Id { get; set; } 10 | 11 | public int ListId { get; set; } 12 | 13 | public string Title { get; set; } 14 | 15 | public bool Done { get; set; } 16 | 17 | public int Priority { get; set; } 18 | 19 | public string Note { get; set; } 20 | 21 | public void Mapping(Profile profile) => 22 | profile.CreateMap() 23 | .ForMember(d => d.Priority, opt => opt.MapFrom(s => (int)s.Priority)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Application/Common/Exceptions/ValidationException.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.Common.Exceptions 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using FluentValidation.Results; 7 | 8 | public class ValidationException : Exception 9 | { 10 | public ValidationException() 11 | : base("One or more validation failures have occurred.") => this.Errors = new Dictionary(); 12 | 13 | public ValidationException(IEnumerable failures) 14 | : this() => this.Errors = failures 15 | .GroupBy(e => e.PropertyName, e => e.ErrorMessage) 16 | .ToDictionary(failureGroup => failureGroup.Key, failureGroup => failureGroup.ToArray()); 17 | 18 | public IDictionary Errors { get; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Application/TodoItems/Queries/GetTodoItemsWithPagination/GetTodoItemsWithPaginationQueryValidator.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.TodoItems.Queries.GetTodoItemsWithPagination 2 | { 3 | using FluentValidation; 4 | 5 | public class GetTodoItemsWithPaginationQueryValidator : AbstractValidator 6 | { 7 | public GetTodoItemsWithPaginationQueryValidator() 8 | { 9 | this.RuleFor(x => x.ListId) 10 | .NotNull() 11 | .NotEmpty().WithMessage("ListId is required."); 12 | 13 | this.RuleFor(x => x.PageNumber) 14 | .GreaterThanOrEqualTo(1).WithMessage("PageNumber at least greater than or equal to 1."); 15 | 16 | this.RuleFor(x => x.PageSize) 17 | .GreaterThanOrEqualTo(1).WithMessage("PageSize at least greater than or equal to 1."); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Application/Application.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | CleanCli.Todo.Application 6 | CleanCli.Todo.Application 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Application/Common/Mappings/MappingExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.Common.Mappings 2 | { 3 | using AutoMapper; 4 | using AutoMapper.QueryableExtensions; 5 | using CleanCli.Todo.Application.Common.Models; 6 | using Microsoft.EntityFrameworkCore; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Threading.Tasks; 10 | 11 | public static class MappingExtensions 12 | { 13 | public static Task> PaginatedListAsync( 14 | this IQueryable queryable, int pageNumber, int pageSize) => 15 | PaginatedList.CreateAsync(queryable, pageNumber, pageSize); 16 | 17 | public static Task> ProjectToListAsync( 18 | this IQueryable queryable, IConfigurationProvider configuration) => 19 | queryable.ProjectTo(configuration).ToListAsync(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Application/TodoLists/Commands/PurgeTodoLists/PurgeTodoListsCommand.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.TodoLists.Commands.PurgeTodoLists 2 | { 3 | using CleanCli.Todo.Application.Common.Interfaces; 4 | using MediatR; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | public class PurgeTodoListsCommand : IRequest 9 | { 10 | } 11 | 12 | public class PurgeTodoListsCommandHandler : IRequestHandler 13 | { 14 | private readonly IApplicationDbContext context; 15 | 16 | public PurgeTodoListsCommandHandler(IApplicationDbContext context) => this.context = context; 17 | 18 | public async Task Handle(PurgeTodoListsCommand request, CancellationToken cancellationToken) 19 | { 20 | this.context.TodoLists.RemoveRange(this.context.TodoLists); 21 | 22 | await this.context.SaveChangesAsync(cancellationToken); 23 | 24 | return Unit.Value; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/CleanCli.Todo.Console/Commands/MigrateCommand.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Console.Commands 2 | { 3 | using System.CommandLine; 4 | using System.CommandLine.Invocation; 5 | using System.Threading.Tasks; 6 | using CleanCli.Todo.Infrastructure.Persistence; 7 | using Microsoft.EntityFrameworkCore; 8 | 9 | public class MigrateCommand : Command 10 | { 11 | public MigrateCommand() 12 | : base(name: "migrate", "Migrates database") 13 | { 14 | } 15 | 16 | public new class Handler : ICommandHandler 17 | { 18 | private readonly ApplicationDbContext db; 19 | 20 | public Handler(ApplicationDbContext db) => this.db = db; 21 | 22 | public async Task InvokeAsync(InvocationContext context) 23 | { 24 | this.db.Database.EnsureCreated(); 25 | this.db.Database.Migrate(); 26 | await ApplicationDbContextSeed.SeedSampleDataAsync(this.db); 27 | return 0; 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Application/TodoItems/EventHandlers/TodoItemCreatedHandler.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.TodoItems.EventHandlers 2 | { 3 | using CleanCli.Todo.Application.Common.Models; 4 | using CleanCli.Todo.Domain.Events; 5 | using MediatR; 6 | using Microsoft.Extensions.Logging; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | public class TodoItemCreatedHandler : INotificationHandler> 11 | { 12 | private readonly ILogger logger; 13 | 14 | public TodoItemCreatedHandler(ILogger logger) => this.logger = logger; 15 | 16 | public Task Handle(DomainEventNotification notification, CancellationToken cancellationToken) 17 | { 18 | var domainEvent = notification.DomainEvent; 19 | 20 | this.logger.LogInformation("CleanArchitectureJT.Example Domain Event: {DomainEvent}", domainEvent.GetType().Name); 21 | 22 | return Task.CompletedTask; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Application/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application 2 | { 3 | using AutoMapper; 4 | using CleanCli.Todo.Application.Common.Behaviours; 5 | using FluentValidation; 6 | using MediatR; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using System.Reflection; 9 | 10 | public static class ServiceCollectionExtensions 11 | { 12 | public static IServiceCollection AddApplication(this IServiceCollection services) 13 | { 14 | services.AddAutoMapper(Assembly.GetExecutingAssembly()); 15 | services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()); 16 | services.AddMediatR(Assembly.GetExecutingAssembly()); 17 | //services.AddTransient(typeof(IPipelineBehavior<,>), typeof(UnhandledExceptionBehaviour<,>)); 18 | services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>)); 19 | services.AddTransient(typeof(IPipelineBehavior<,>), typeof(PerformanceBehaviour<,>)); 20 | 21 | return services; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Application/TodoItems/EventHandlers/TodoItemCompletedHandler.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.TodoItems.EventHandlers 2 | { 3 | using CleanCli.Todo.Application.Common.Models; 4 | using CleanCli.Todo.Domain.Events; 5 | using MediatR; 6 | using Microsoft.Extensions.Logging; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | public class TodoItemCompletedHandler : INotificationHandler> 11 | { 12 | private readonly ILogger logger; 13 | 14 | public TodoItemCompletedHandler(ILogger logger) => this.logger = logger; 15 | 16 | public Task Handle(DomainEventNotification notification, CancellationToken cancellationToken) 17 | { 18 | var domainEvent = notification.DomainEvent; 19 | 20 | this.logger.LogInformation("CleanArchitectureJT.Example Domain Event: {DomainEvent}", domainEvent.GetType().Name); 21 | 22 | return Task.CompletedTask; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/CleanCli.Todo.Console/Commands/SeedTodoItemsCommand.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Console.Commands 2 | { 3 | using System; 4 | using System.CommandLine; 5 | using System.CommandLine.Invocation; 6 | using System.Threading.Tasks; 7 | 8 | public class SeedTodoItemsCommand : Command 9 | { 10 | public IConsole Console { get; set; } 11 | 12 | public SeedTodoItemsCommand() 13 | : base(name: "seed", "Seeds a random todo items.") 14 | { 15 | this.AddOption(new Option( 16 | new string[] { "--todolist", "-l" }, "Title of the todo list")); 17 | this.AddOption(new Option( 18 | new string[] { "--number", "-n" }, 19 | () => 3, 20 | "The number of todo items to be generated.")); 21 | } 22 | 23 | public new class Handler : ICommandHandler 24 | { 25 | public Task InvokeAsync(InvocationContext context) 26 | { 27 | throw new NotImplementedException(); 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Application/Common/Mappings/MappingProfile.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.Common.Mappings 2 | { 3 | using AutoMapper; 4 | using System; 5 | using System.Linq; 6 | using System.Reflection; 7 | 8 | public class MappingProfile : Profile 9 | { 10 | public MappingProfile() => this.ApplyMappingsFromAssembly(Assembly.GetExecutingAssembly()); 11 | 12 | private void ApplyMappingsFromAssembly(Assembly assembly) 13 | { 14 | var types = assembly.GetExportedTypes() 15 | .Where(t => t.GetInterfaces().Any(i => 16 | i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IMapFrom<>))) 17 | .ToList(); 18 | 19 | foreach (var type in types) 20 | { 21 | var instance = Activator.CreateInstance(type); 22 | 23 | var methodInfo = type.GetMethod("Mapping") 24 | ?? type.GetInterface("IMapFrom`1").GetMethod("Mapping"); 25 | methodInfo?.Invoke(instance, new object[] { this }); 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Application/Common/Behaviours/UnhandledExceptionBehaviour.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.Common.Behaviours 2 | { 3 | using MediatR; 4 | using Microsoft.Extensions.Logging; 5 | using System; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | public class UnhandledExceptionBehaviour : IPipelineBehavior 10 | { 11 | private readonly ILogger logger; 12 | 13 | public UnhandledExceptionBehaviour(ILogger logger) => this.logger = logger; 14 | 15 | public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) 16 | { 17 | try 18 | { 19 | return await next(); 20 | } 21 | catch (Exception ex) 22 | { 23 | var requestName = typeof(TRequest).Name; 24 | 25 | this.logger.LogError(ex, "Request: Unhandled Exception for Request {Name} {@Request}", requestName, request); 26 | 27 | throw; 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/CleanCli.Todo.Console/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/appsettings.json", 3 | "ConnectionStrings": { 4 | "DefaultConnection": "Data Source=todo.db" 5 | }, 6 | "Serilog": { 7 | "Using": [ 8 | "Serilog", 9 | "Serilog.Sinks.Console", 10 | "Serilog.Exceptions" 11 | ], 12 | "MinimumLevel": { 13 | "Default": "Verbose", 14 | "Override": { 15 | "System": "Warning", 16 | "Microsoft": "Information", 17 | "Microsoft.Hosting.Lifetime": "Warning", 18 | "Microsoft.EntityFrameworkCore.Infrastructure": "Warning" 19 | } 20 | }, 21 | "WriteTo": [ 22 | { 23 | "Name": "Async", 24 | "Args": { 25 | "configure": [ 26 | { 27 | "Name": "Console", 28 | "Args": { 29 | "restrictedToMinimumLevel": "Debug", 30 | "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3} {SourceContext}] {Message}{NewLine}{Exception}" 31 | } 32 | } 33 | ] 34 | } 35 | } 36 | ], 37 | "Enrich": [ "WithExceptionDetails" ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Infrastructure/Persistence/ApplicationDbContextSeed.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Infrastructure.Persistence 2 | { 3 | using CleanCli.Todo.Domain.Entities; 4 | using CleanCli.Todo.Domain.ValueObjects; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | public static class ApplicationDbContextSeed 9 | { 10 | 11 | public static async Task SeedSampleDataAsync(ApplicationDbContext context) 12 | { 13 | // Seed, if necessary 14 | if (!context.TodoLists.Any()) 15 | { 16 | context.TodoLists.Add(new TodoList 17 | { 18 | Title = "Shopping", 19 | Colour = Colour.Blue, 20 | Items = 21 | { 22 | new TodoItem { Title = "Milk", Done = true }, 23 | new TodoItem { Title = "Bread", Done = true }, 24 | new TodoItem { Title = "Pasta" }, 25 | new TodoItem { Title = "Water" } 26 | } 27 | }); 28 | 29 | await context.SaveChangesAsync(); 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Application/TodoLists/Commands/CreateTodoList/CreateTodoListCommand.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.TodoLists.Commands.CreateTodoList 2 | { 3 | using CleanCli.Todo.Application.Common.Interfaces; 4 | using CleanCli.Todo.Domain.Entities; 5 | using MediatR; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | public class CreateTodoListCommand : IRequest 10 | { 11 | public string Title { get; set; } 12 | } 13 | 14 | public class CreateTodoListCommandHandler : IRequestHandler 15 | { 16 | private readonly IApplicationDbContext context; 17 | 18 | public CreateTodoListCommandHandler(IApplicationDbContext context) => this.context = context; 19 | 20 | public async Task Handle(CreateTodoListCommand request, CancellationToken cancellationToken) 21 | { 22 | var entity = new TodoList 23 | { 24 | Title = request.Title 25 | }; 26 | 27 | this.context.TodoLists.Add(entity); 28 | 29 | await this.context.SaveChangesAsync(cancellationToken); 30 | 31 | return entity.Id; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Application/TodoLists/Commands/CreateTodoList/CreateTodoListCommandValidator.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.TodoLists.Commands.CreateTodoList 2 | { 3 | using CleanCli.Todo.Application.Common.Interfaces; 4 | using FluentValidation; 5 | using Microsoft.EntityFrameworkCore; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | public class CreateTodoListCommandValidator : AbstractValidator 10 | { 11 | private readonly IApplicationDbContext context; 12 | 13 | public CreateTodoListCommandValidator(IApplicationDbContext context) 14 | { 15 | this.context = context; 16 | 17 | this.RuleFor(v => v.Title) 18 | .NotEmpty().WithMessage("Title is required.") 19 | .MaximumLength(200).WithMessage("Title must not exceed 200 characters.") 20 | .MustAsync(this.BeUniqueTitle).WithMessage("The specified title already exists."); 21 | } 22 | 23 | public async Task BeUniqueTitle(string title, CancellationToken cancellationToken) => 24 | await this.context.TodoLists.AllAsync(l => l.Title != title, cancellationToken: cancellationToken); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Infrastructure/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Infrastructure 2 | { 3 | using CleanCli.Todo.Application.Common.Interfaces; 4 | using CleanCli.Todo.Infrastructure.Persistence; 5 | using CleanCli.Todo.Infrastructure.Services; 6 | using Microsoft.EntityFrameworkCore; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.DependencyInjection; 9 | 10 | public static class ServiceCollectionExtensions 11 | { 12 | public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration) 13 | { 14 | services.AddDbContext(options => 15 | options.UseSqlite(configuration.GetConnectionString("DefaultConnection"), 16 | b => b.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName))); 17 | 18 | services.AddScoped(provider => provider.GetService()); 19 | 20 | services.AddScoped(); 21 | 22 | services.AddTransient(); 23 | 24 | return services; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Domain/Entities/TodoItem.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Domain.Entities 2 | { 3 | using CleanCli.Todo.Domain.Common; 4 | using CleanCli.Todo.Domain.Enums; 5 | using CleanCli.Todo.Domain.Events; 6 | using System; 7 | using System.Collections.Generic; 8 | 9 | public class TodoItem : AuditableEntity, IHasDomainEvent 10 | { 11 | public int Id { get; set; } 12 | 13 | public TodoList List { get; set; } 14 | 15 | public int ListId { get; set; } 16 | 17 | public string Title { get; set; } 18 | 19 | public string Note { get; set; } 20 | 21 | public PriorityLevel Priority { get; set; } 22 | 23 | public DateTime? Reminder { get; set; } 24 | 25 | private bool done; 26 | public bool Done 27 | { 28 | get => this.done; 29 | set 30 | { 31 | if (value == true && this.done == false) 32 | { 33 | this.DomainEvents.Add(new TodoItemCompletedEvent(this)); 34 | } 35 | 36 | this.done = value; 37 | } 38 | } 39 | 40 | public List DomainEvents { get; set; } = new List(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | cobertura 6 | 7 | 8 | 9 | 10 | all 11 | runtime; build; native; contentfiles; analyzers; buildtransitive 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Application/Common/Behaviours/LoggingBehaviour.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.Common.Behaviours 2 | { 3 | using CleanCli.Todo.Application.Common.Interfaces; 4 | using MediatR.Pipeline; 5 | using Microsoft.Extensions.Logging; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | public class LoggingBehaviour : IRequestPreProcessor 10 | { 11 | private readonly ILogger logger; 12 | private readonly ICurrentUserService currentUserService; 13 | 14 | public LoggingBehaviour(ILogger logger, ICurrentUserService currentUserService) 15 | { 16 | this.logger = logger; 17 | this.currentUserService = currentUserService; 18 | } 19 | 20 | public Task Process(TRequest request, CancellationToken cancellationToken) 21 | { 22 | var requestName = typeof(TRequest).Name; 23 | var userId = this.currentUserService.UserId ?? string.Empty; 24 | var userName = string.Empty; 25 | 26 | this.logger.LogInformation("Request: {Name} {@UserId} {@UserName} {@Request}", 27 | requestName, userId, userName, request); 28 | return Task.CompletedTask; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Infrastructure/Infrastructure.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | CleanCli.Todo.Infrastructure 6 | CleanCli.Todo.Infrastructure 7 | True 8 | 9 | 10 | 11 | 12 | 13 | 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/CleanCli.Todo.Console/Commands/DeleteTodoListCommand.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Console.Commands 2 | { 3 | using System; 4 | using System.CommandLine; 5 | using System.CommandLine.Invocation; 6 | using System.Threading.Tasks; 7 | using MediatR; 8 | 9 | public class DeleteTodoListCommand : Command 10 | { 11 | public IConsole Console { get; set; } 12 | 13 | public DeleteTodoListCommand() 14 | : base(name: "delete", "Deletes todo list") 15 | { 16 | this.AddArgument(new Argument("id", "Id of the todo list")); 17 | } 18 | 19 | public new class Handler : ICommandHandler 20 | { 21 | private readonly IMediator meditor; 22 | 23 | public int Id { get; set; } 24 | 25 | public Handler(IMediator meditor) => 26 | this.meditor = meditor ?? throw new ArgumentNullException(nameof(meditor)); 27 | 28 | public async Task InvokeAsync(InvocationContext context) 29 | { 30 | await this.meditor.Send(new Application.TodoLists.Commands.DeleteTodoList.DeleteTodoListCommand 31 | { 32 | Id = this.Id, 33 | }); 34 | 35 | return 0; 36 | } 37 | } 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /src/Application/TodoLists/Commands/UpdateTodoList/UpdateTodoListCommandValidator.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.TodoLists.Commands.UpdateTodoList 2 | { 3 | using CleanCli.Todo.Application.Common.Interfaces; 4 | using FluentValidation; 5 | using Microsoft.EntityFrameworkCore; 6 | using System.Linq; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | public class UpdateTodoListCommandValidator : AbstractValidator 11 | { 12 | private readonly IApplicationDbContext context; 13 | 14 | public UpdateTodoListCommandValidator(IApplicationDbContext context) 15 | { 16 | this.context = context; 17 | 18 | this.RuleFor(v => v.Title) 19 | .NotEmpty().WithMessage("Title is required.") 20 | .MaximumLength(200).WithMessage("Title must not exceed 200 characters.") 21 | .MustAsync(this.BeUniqueTitle).WithMessage("The specified title already exists."); 22 | } 23 | 24 | public async Task BeUniqueTitle(UpdateTodoListCommand model, string title, CancellationToken cancellationToken) => 25 | await this.context.TodoLists 26 | .Where(l => l.Id != model.Id) 27 | .AllAsync(l => l.Title != title, cancellationToken: cancellationToken); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: [ main ] 5 | pull_request: 6 | release: 7 | types: 8 | - published 9 | 10 | env: 11 | # Disable the .NET logo in the console output. 12 | DOTNET_NOLOGO: true 13 | # Disable the .NET first time experience to skip caching NuGet packages and speed up the build. 14 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 15 | # Disable sending .NET CLI telemetry to Microsoft. 16 | DOTNET_CLI_TELEMETRY_OPTOUT: true 17 | # Set the build number in MinVer. 18 | MINVERBUILDMETADATA: build.${{github.run_number}} 19 | 20 | jobs: 21 | build: 22 | name: Build-${{matrix.os}} 23 | runs-on: ${{matrix.os}} 24 | strategy: 25 | matrix: 26 | os: [ubuntu-latest, windows-latest, macOS-latest] 27 | steps: 28 | - name: 'Checkout' 29 | uses: actions/checkout@v2 30 | with: 31 | lfs: true 32 | fetch-depth: 0 33 | - name: 'Git Fetch Tags' 34 | run: git fetch --tags 35 | shell: pwsh 36 | - name: 'Install .NET Core SDK' 37 | uses: actions/setup-dotnet@v1 38 | - name: 'Dotnet Tool Restore' 39 | run: dotnet tool restore 40 | shell: pwsh 41 | - name: 'Dotnet Cake Build' 42 | run: dotnet cake --target=Build 43 | shell: pwsh 44 | - name: 'Dotnet Cake Test' 45 | run: dotnet cake --target=Test 46 | shell: pwsh 47 | -------------------------------------------------------------------------------- /src/Infrastructure/Services/DomainEventService.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Infrastructure.Services 2 | { 3 | using CleanCli.Todo.Application.Common.Interfaces; 4 | using CleanCli.Todo.Application.Common.Models; 5 | using CleanCli.Todo.Domain.Common; 6 | using MediatR; 7 | using Microsoft.Extensions.Logging; 8 | using System; 9 | using System.Threading.Tasks; 10 | 11 | public class DomainEventService : IDomainEventService 12 | { 13 | private readonly ILogger logger; 14 | private readonly IPublisher mediator; 15 | 16 | public DomainEventService(ILogger logger, IPublisher mediator) 17 | { 18 | this.logger = logger; 19 | this.mediator = mediator; 20 | } 21 | 22 | public async Task Publish(DomainEvent domainEvent) 23 | { 24 | this.logger.LogInformation("Publishing domain event. Event - {event}", domainEvent.GetType().Name); 25 | await this.mediator.Publish(GetNotificationCorrespondingToDomainEvent(domainEvent)); 26 | } 27 | 28 | private static INotification GetNotificationCorrespondingToDomainEvent(DomainEvent domainEvent) => 29 | (INotification)Activator 30 | .CreateInstance(typeof(DomainEventNotification<>) 31 | .MakeGenericType(domainEvent.GetType()), domainEvent); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Domain/Common/ValueObject.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Domain.Common 2 | { 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | // Learn more: https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/microservice-ddd-cqrs-patterns/implement-value-objects 7 | public abstract class ValueObject 8 | { 9 | protected static bool EqualOperator(ValueObject left, ValueObject right) 10 | { 11 | if (left is null ^ right is null) 12 | { 13 | return false; 14 | } 15 | 16 | return left?.Equals(right) != false; 17 | } 18 | 19 | protected static bool NotEqualOperator(ValueObject left, ValueObject right) => !EqualOperator(left, right); 20 | 21 | protected abstract IEnumerable GetEqualityComponents(); 22 | 23 | public override bool Equals(object obj) 24 | { 25 | if (obj == null || obj.GetType() != this.GetType()) 26 | { 27 | return false; 28 | } 29 | 30 | var other = (ValueObject)obj; 31 | return this.GetEqualityComponents().SequenceEqual(other.GetEqualityComponents()); 32 | } 33 | 34 | public override int GetHashCode() => this.GetEqualityComponents() 35 | .Select(x => x != null ? x.GetHashCode() : 0) 36 | .Aggregate((x, y) => x ^ y); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Application/TodoItems/Commands/DeleteTodoItem/DeleteTodoItemCommand.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.TodoItems.Commands.DeleteTodoItem 2 | { 3 | using CleanCli.Todo.Application.Common.Exceptions; 4 | using CleanCli.Todo.Application.Common.Interfaces; 5 | using CleanCli.Todo.Domain.Entities; 6 | using MediatR; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | public class DeleteTodoItemCommand : IRequest 11 | { 12 | public int Id { get; set; } 13 | } 14 | 15 | public class DeleteTodoItemCommandHandler : IRequestHandler 16 | { 17 | private readonly IApplicationDbContext context; 18 | 19 | public DeleteTodoItemCommandHandler(IApplicationDbContext context) => this.context = context; 20 | 21 | public async Task Handle(DeleteTodoItemCommand request, CancellationToken cancellationToken) 22 | { 23 | var entity = await this.context.TodoItems 24 | .FindAsync(new object[] { request.Id }, cancellationToken: cancellationToken); 25 | 26 | if (entity == null) 27 | { 28 | throw new NotFoundException(nameof(TodoItem), request.Id); 29 | } 30 | 31 | this.context.TodoItems.Remove(entity); 32 | 33 | await this.context.SaveChangesAsync(cancellationToken); 34 | 35 | return Unit.Value; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Application/TodoItems/Commands/CreateTodoItem/CreateTodoItemCommand.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.TodoItems.Commands.CreateTodoItem 2 | { 3 | using CleanCli.Todo.Application.Common.Interfaces; 4 | using CleanCli.Todo.Domain.Entities; 5 | using CleanCli.Todo.Domain.Events; 6 | using MediatR; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | public class CreateTodoItemCommand : IRequest 11 | { 12 | public int ListId { get; set; } 13 | 14 | public string Title { get; set; } 15 | } 16 | 17 | public class CreateTodoItemCommandHandler : IRequestHandler 18 | { 19 | private readonly IApplicationDbContext context; 20 | 21 | public CreateTodoItemCommandHandler(IApplicationDbContext context) => this.context = context; 22 | 23 | public async Task Handle(CreateTodoItemCommand request, CancellationToken cancellationToken) 24 | { 25 | var entity = new TodoItem 26 | { 27 | ListId = request.ListId, 28 | Title = request.Title, 29 | Done = false 30 | }; 31 | 32 | entity.DomainEvents.Add(new TodoItemCreatedEvent(entity)); 33 | 34 | this.context.TodoItems.Add(entity); 35 | 36 | await this.context.SaveChangesAsync(cancellationToken); 37 | 38 | return entity.Id; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Application/TodoLists/Commands/UpdateTodoList/UpdateTodoListCommand.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.TodoLists.Commands.UpdateTodoList 2 | { 3 | using CleanCli.Todo.Application.Common.Exceptions; 4 | using CleanCli.Todo.Application.Common.Interfaces; 5 | using CleanCli.Todo.Domain.Entities; 6 | using MediatR; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | public class UpdateTodoListCommand : IRequest 11 | { 12 | public int Id { get; set; } 13 | 14 | public string Title { get; set; } 15 | } 16 | 17 | public class UpdateTodoListCommandHandler : IRequestHandler 18 | { 19 | private readonly IApplicationDbContext context; 20 | 21 | public UpdateTodoListCommandHandler(IApplicationDbContext context) => this.context = context; 22 | 23 | public async Task Handle(UpdateTodoListCommand request, CancellationToken cancellationToken) 24 | { 25 | var entity = await this.context.TodoLists.FindAsync(new object[] { request.Id }, cancellationToken: cancellationToken); 26 | 27 | if (entity == null) 28 | { 29 | throw new NotFoundException(nameof(TodoList), request.Id); 30 | } 31 | 32 | entity.Title = request.Title; 33 | 34 | await this.context.SaveChangesAsync(cancellationToken); 35 | 36 | return Unit.Value; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################### 2 | # Git Line Endings # 3 | ############################### 4 | 5 | # Set default behavior to automatically normalize line endings. 6 | * text=auto 7 | 8 | # Force batch scripts to always use CRLF line endings so that if a repo is accessed 9 | # in Windows via a file share from Linux, the scripts will work. 10 | *.{cmd,[cC][mM][dD]} text eol=crlf 11 | *.{bat,[bB][aA][tT]} text eol=crlf 12 | 13 | # Force bash scripts to always use LF line endings so that if a repo is accessed 14 | # in Unix via a file share from Windows, the scripts will work. 15 | *.sh text eol=lf 16 | 17 | ############################### 18 | # Git Large File System (LFS) # 19 | ############################### 20 | 21 | # Archives 22 | *.7z filter=lfs diff=lfs merge=lfs -text 23 | *.br filter=lfs diff=lfs merge=lfs -text 24 | *.gz filter=lfs diff=lfs merge=lfs -text 25 | *.tar filter=lfs diff=lfs merge=lfs -text 26 | *.zip filter=lfs diff=lfs merge=lfs -text 27 | 28 | # Documents 29 | *.pdf filter=lfs diff=lfs merge=lfs -text 30 | 31 | # Images 32 | *.gif filter=lfs diff=lfs merge=lfs -text 33 | *.ico filter=lfs diff=lfs merge=lfs -text 34 | *.jpg filter=lfs diff=lfs merge=lfs -text 35 | *.png filter=lfs diff=lfs merge=lfs -text 36 | *.psd filter=lfs diff=lfs merge=lfs -text 37 | *.webp filter=lfs diff=lfs merge=lfs -text 38 | 39 | # Fonts 40 | *.woff2 filter=lfs diff=lfs merge=lfs -text 41 | 42 | # Other 43 | *.exe filter=lfs diff=lfs merge=lfs -text 44 | -------------------------------------------------------------------------------- /src/Application/TodoLists/Commands/DeleteTodoList/DeleteTodoListCommand.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.TodoLists.Commands.DeleteTodoList 2 | { 3 | using CleanCli.Todo.Application.Common.Exceptions; 4 | using CleanCli.Todo.Application.Common.Interfaces; 5 | using CleanCli.Todo.Domain.Entities; 6 | using MediatR; 7 | using Microsoft.EntityFrameworkCore; 8 | using System.Linq; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | public class DeleteTodoListCommand : IRequest 13 | { 14 | public int Id { get; set; } 15 | } 16 | 17 | public class DeleteTodoListCommandHandler : IRequestHandler 18 | { 19 | private readonly IApplicationDbContext context; 20 | 21 | public DeleteTodoListCommandHandler(IApplicationDbContext context) => this.context = context; 22 | 23 | public async Task Handle(DeleteTodoListCommand request, CancellationToken cancellationToken) 24 | { 25 | var entity = await this.context.TodoLists 26 | .Where(l => l.Id == request.Id) 27 | .SingleOrDefaultAsync(cancellationToken); 28 | 29 | if (entity == null) 30 | { 31 | throw new NotFoundException(nameof(TodoList), request.Id); 32 | } 33 | 34 | this.context.TodoLists.Remove(entity); 35 | 36 | await this.context.SaveChangesAsync(cancellationToken); 37 | 38 | return Unit.Value; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Application/Common/Models/PaginatedList.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.Common.Models 2 | { 3 | using Microsoft.EntityFrameworkCore; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | 9 | public class PaginatedList 10 | { 11 | public List Items { get; } 12 | public int PageIndex { get; } 13 | public int TotalPages { get; } 14 | public int TotalCount { get; } 15 | 16 | public PaginatedList(List items, int count, int pageIndex, int pageSize) 17 | { 18 | this.PageIndex = pageIndex; 19 | this.TotalPages = (int)Math.Ceiling(count / (double)pageSize); 20 | this.TotalCount = count; 21 | this.Items = items; 22 | } 23 | 24 | public bool HasPreviousPage => this.PageIndex > 1; 25 | 26 | public bool HasNextPage => this.PageIndex < this.TotalPages; 27 | 28 | #pragma warning disable CA1000 // Do not declare static members on generic types 29 | public static async Task> CreateAsync(IQueryable source, int pageIndex, int pageSize) 30 | { 31 | var count = await source.CountAsync(); 32 | var items = await source.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync(); 33 | 34 | return new PaginatedList(items, count, pageIndex, pageSize); 35 | } 36 | #pragma warning restore CA1000 // Do not declare static members on generic types 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Application/Common/Behaviours/ValidationBehaviour.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.Common.Behaviours 2 | { 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using FluentValidation; 8 | using MediatR; 9 | using ValidationException = CleanCli.Todo.Application.Common.Exceptions.ValidationException; 10 | 11 | public class ValidationBehaviour : IPipelineBehavior 12 | where TRequest : IRequest 13 | { 14 | private readonly IEnumerable> validators; 15 | 16 | public ValidationBehaviour(IEnumerable> validators) => this.validators = validators; 17 | 18 | public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) 19 | { 20 | if (this.validators.Any()) 21 | { 22 | var context = new ValidationContext(request); 23 | 24 | var validationResults = await Task.WhenAll(this.validators.Select(v => v.ValidateAsync(context, cancellationToken))); 25 | var failures = validationResults.SelectMany(r => r.Errors).Where(f => f != null).ToList(); 26 | 27 | if (failures.Count != 0) 28 | { 29 | throw new ValidationException(failures); 30 | } 31 | } 32 | return await next(); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/CleanCli.Todo.Console/Commands/CreateTodoListCommand.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Console.Commands 2 | { 3 | using System; 4 | using System.CommandLine; 5 | using System.CommandLine.Invocation; 6 | using System.Threading.Tasks; 7 | using MediatR; 8 | 9 | public class CreateTodoListCommand : Command 10 | { 11 | public IConsole Console { get; set; } 12 | 13 | public CreateTodoListCommand() 14 | : base(name: "create", "Creates todo list") 15 | { 16 | this.AddOption(new Option( 17 | new string[] { "--title", "-t" }, "Title of the todo list")); 18 | this.AddOption(new Option( 19 | "--dry-run", "Displays a summary of what would happen if the given command line were run.")); 20 | } 21 | 22 | public new class Handler : ICommandHandler 23 | { 24 | private readonly IMediator meditor; 25 | 26 | public string Title { get; set; } 27 | 28 | public Handler(IMediator meditor) => 29 | this.meditor = meditor ?? throw new ArgumentNullException(nameof(meditor)); 30 | 31 | public async Task InvokeAsync(InvocationContext context) 32 | { 33 | await this.meditor.Send( 34 | new Application.TodoLists.Commands.CreateTodoList.CreateTodoListCommand 35 | { 36 | Title = this.Title, 37 | }); 38 | 39 | return 0; 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Application/TodoItems/Commands/UpdateTodoItem/UpdateTodoItemCommand.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.TodoItems.Commands.UpdateTodoItem 2 | { 3 | using CleanCli.Todo.Application.Common.Exceptions; 4 | using CleanCli.Todo.Application.Common.Interfaces; 5 | using CleanCli.Todo.Domain.Entities; 6 | using MediatR; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | public class UpdateTodoItemCommand : IRequest 11 | { 12 | public int Id { get; set; } 13 | 14 | public string Title { get; set; } 15 | 16 | public bool Done { get; set; } 17 | } 18 | 19 | public class UpdateTodoItemCommandHandler : IRequestHandler 20 | { 21 | private readonly IApplicationDbContext context; 22 | 23 | public UpdateTodoItemCommandHandler(IApplicationDbContext context) => this.context = context; 24 | 25 | public async Task Handle(UpdateTodoItemCommand request, CancellationToken cancellationToken) 26 | { 27 | var entity = await this.context.TodoItems.FindAsync( 28 | new object[] { request.Id }, cancellationToken: cancellationToken); 29 | 30 | if (entity == null) 31 | { 32 | throw new NotFoundException(nameof(TodoItem), request.Id); 33 | } 34 | 35 | entity.Title = request.Title; 36 | entity.Done = request.Done; 37 | 38 | await this.context.SaveChangesAsync(cancellationToken); 39 | 40 | return Unit.Value; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/CleanCli.Todo.Console/Commands/GetTodoListCommand.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Console.Commands 2 | { 3 | using System; 4 | using System.CommandLine; 5 | using System.CommandLine.Invocation; 6 | using System.Threading.Tasks; 7 | using CleanCli.Todo.Application.TodoLists.Queries.GetTodo; 8 | using MediatR; 9 | using Spectre.Console; 10 | 11 | public class GetTodoListCommand : Command 12 | { 13 | public IConsole Console { get; set; } 14 | 15 | public GetTodoListCommand() 16 | : base(name: "get", "Gets a todo list") 17 | { 18 | this.AddArgument(new Argument("id", "Id of the todo list")); 19 | } 20 | 21 | public new class Handler : ICommandHandler 22 | { 23 | private readonly IMediator meditor; 24 | 25 | public Handler(IMediator meditor) => 26 | this.meditor = meditor ?? throw new ArgumentNullException(nameof(meditor)); 27 | 28 | public int Id { get; set; } 29 | 30 | public async Task InvokeAsync(InvocationContext context) 31 | { 32 | var list = await this.meditor.Send(new GetTodoQuery { Id = this.Id }); 33 | var dump = ObjectDumper.Dump(list, DumpStyle.Console); 34 | 35 | var panel = new Panel(dump) 36 | { 37 | Border = BoxBorder.Rounded, 38 | Header = new PanelHeader($"{list.Title}", Justify.Right), 39 | }; 40 | AnsiConsole.Render(panel); 41 | return 0; 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Application/TodoLists/Queries/GetTodo/GetTodoQuery.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.TodoLists.Queries.GetTodo 2 | { 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using AutoMapper; 7 | using CleanCli.Todo.Application.Common.Exceptions; 8 | using CleanCli.Todo.Application.Common.Interfaces; 9 | using CleanCli.Todo.Application.TodoLists.Queries.GetTodos; 10 | using CleanCli.Todo.Domain.Entities; 11 | using MediatR; 12 | using Microsoft.EntityFrameworkCore; 13 | 14 | public class GetTodoQuery : IRequest 15 | { 16 | public int Id { get; set; } 17 | } 18 | 19 | public class GetTodoQueryHandler : IRequestHandler 20 | { 21 | private readonly IApplicationDbContext context; 22 | private readonly IMapper mapper; 23 | 24 | public GetTodoQueryHandler(IApplicationDbContext context, IMapper mapper) 25 | { 26 | this.context = context; 27 | this.mapper = mapper; 28 | } 29 | 30 | public async Task Handle(GetTodoQuery request, CancellationToken cancellationToken) 31 | { 32 | var entity = await this.context.TodoLists 33 | .Include(t => t.Items) 34 | .Where(l => l.Id == request.Id) 35 | .SingleOrDefaultAsync(cancellationToken); 36 | 37 | if (entity == null) 38 | { 39 | throw new NotFoundException(nameof(TodoList), request.Id); 40 | } 41 | 42 | return this.mapper.Map(entity); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Application/TodoLists/Queries/GetTodos/GetTodosQuery.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.TodoLists.Queries.GetTodos 2 | { 3 | using AutoMapper; 4 | using AutoMapper.QueryableExtensions; 5 | using CleanCli.Todo.Application.Common.Interfaces; 6 | using CleanCli.Todo.Domain.Enums; 7 | using MediatR; 8 | using Microsoft.EntityFrameworkCore; 9 | using System; 10 | using System.Linq; 11 | using System.Threading; 12 | using System.Threading.Tasks; 13 | 14 | public class GetTodosQuery : IRequest 15 | { 16 | } 17 | 18 | public class GetTodosQueryHandler : IRequestHandler 19 | { 20 | private readonly IApplicationDbContext context; 21 | private readonly IMapper mapper; 22 | 23 | public GetTodosQueryHandler(IApplicationDbContext context, IMapper mapper) 24 | { 25 | this.context = context; 26 | this.mapper = mapper; 27 | } 28 | 29 | public async Task Handle(GetTodosQuery request, CancellationToken cancellationToken) => new TodosVm 30 | { 31 | PriorityLevels = Enum.GetValues(typeof(PriorityLevel)) 32 | .Cast() 33 | .Select(p => new PriorityLevelDto { Value = (int)p, Name = p.ToString() }) 34 | .ToList(), 35 | 36 | Lists = await this.context.TodoLists 37 | .Include(t => t.Items) 38 | .AsNoTracking() 39 | .ProjectTo(this.mapper.ConfigurationProvider) 40 | .OrderBy(t => t.Title) 41 | .ToListAsync(cancellationToken) 42 | }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Application/TodoItems/Commands/UpdateTodoItemDetail/UpdateTodoItemDetailCommand.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.TodoItems.Commands.UpdateTodoItemDetail 2 | { 3 | using CleanCli.Todo.Application.Common.Exceptions; 4 | using CleanCli.Todo.Application.Common.Interfaces; 5 | using CleanCli.Todo.Domain.Entities; 6 | using CleanCli.Todo.Domain.Enums; 7 | using MediatR; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | public class UpdateTodoItemDetailCommand : IRequest 12 | { 13 | public int Id { get; set; } 14 | 15 | public int ListId { get; set; } 16 | 17 | public PriorityLevel Priority { get; set; } 18 | 19 | public string Note { get; set; } 20 | } 21 | 22 | public class UpdateTodoItemDetailCommandHandler : IRequestHandler 23 | { 24 | private readonly IApplicationDbContext context; 25 | 26 | public UpdateTodoItemDetailCommandHandler(IApplicationDbContext context) => this.context = context; 27 | 28 | public async Task Handle(UpdateTodoItemDetailCommand request, CancellationToken cancellationToken) 29 | { 30 | var entity = await this.context.TodoItems.FindAsync( 31 | new object[] { request.Id }, cancellationToken: cancellationToken); 32 | 33 | if (entity == null) 34 | { 35 | throw new NotFoundException(nameof(TodoItem), request.Id); 36 | } 37 | 38 | entity.ListId = request.ListId; 39 | entity.Priority = request.Priority; 40 | entity.Note = request.Note; 41 | 42 | await this.context.SaveChangesAsync(cancellationToken); 43 | 44 | return Unit.Value; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Application/TodoItems/Queries/GetTodoItemsWithPagination/GetTodoItemsWithPaginationQuery.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.TodoItems.Queries.GetTodoItemsWithPagination 2 | { 3 | using AutoMapper; 4 | using AutoMapper.QueryableExtensions; 5 | using CleanCli.Todo.Application.Common.Interfaces; 6 | using CleanCli.Todo.Application.Common.Mappings; 7 | using CleanCli.Todo.Application.Common.Models; 8 | using CleanCli.Todo.Application.TodoLists.Queries.GetTodos; 9 | using MediatR; 10 | using System.Linq; 11 | using System.Threading; 12 | using System.Threading.Tasks; 13 | 14 | public class GetTodoItemsWithPaginationQuery : IRequest> 15 | { 16 | public int ListId { get; set; } 17 | public int PageNumber { get; set; } = 1; 18 | public int PageSize { get; set; } = 10; 19 | } 20 | 21 | public class GetTodoItemsWithPaginationQueryHandler : IRequestHandler> 22 | { 23 | private readonly IApplicationDbContext _context; 24 | private readonly IMapper _mapper; 25 | 26 | public GetTodoItemsWithPaginationQueryHandler(IApplicationDbContext context, IMapper mapper) 27 | { 28 | _context = context; 29 | _mapper = mapper; 30 | } 31 | 32 | public async Task> Handle(GetTodoItemsWithPaginationQuery request, CancellationToken cancellationToken) 33 | { 34 | return await _context.TodoItems 35 | .Where(x => x.ListId == request.ListId) 36 | .OrderBy(x => x.Title) 37 | .ProjectTo(_mapper.ConfigurationProvider) 38 | .PaginatedListAsync(request.PageNumber, request.PageSize); ; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Application/Common/Behaviours/PerformanceBehaviour.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Application.Common.Behaviours 2 | { 3 | using MediatR; 4 | using Microsoft.Extensions.Logging; 5 | using CleanCli.Todo.Application.Common.Interfaces; 6 | using System.Diagnostics; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | public class PerformanceBehaviour : IPipelineBehavior 11 | { 12 | private readonly Stopwatch timer; 13 | private readonly ILogger logger; 14 | private readonly ICurrentUserService currentUserService; 15 | 16 | public PerformanceBehaviour( 17 | ILogger logger, 18 | ICurrentUserService currentUserService) 19 | { 20 | this.timer = new Stopwatch(); 21 | 22 | this.logger = logger; 23 | this.currentUserService = currentUserService; 24 | } 25 | 26 | public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) 27 | { 28 | this.timer.Start(); 29 | 30 | var response = await next(); 31 | 32 | this.timer.Stop(); 33 | 34 | var elapsedMilliseconds = this.timer.ElapsedMilliseconds; 35 | 36 | if (elapsedMilliseconds > 500) 37 | { 38 | var requestName = typeof(TRequest).Name; 39 | var userId = this.currentUserService.UserId ?? string.Empty; 40 | var userName = string.Empty; 41 | 42 | this.logger.LogWarning("Long Running Request: {Name} ({ElapsedMilliseconds} milliseconds) {@UserId} {@UserName} {@Request}", 43 | requestName, elapsedMilliseconds, userId, userName, request); 44 | } 45 | 46 | return response; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/CleanCli.Todo.Console/CleanCli.Todo.Console.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | Exe 6 | CleanCli.Todo.Console 7 | CleanCli.Todo.Console 8 | 9 | 10 | 11 | 12 | PreserveNewest 13 | true 14 | PreserveNewest 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/CleanCli.Todo.Console/Commands/ListTodosCommand.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Console.Commands 2 | { 3 | using System; 4 | using System.CommandLine; 5 | using System.CommandLine.Invocation; 6 | using System.Globalization; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | using CleanCli.Todo.Application.TodoLists.Queries.GetTodos; 10 | using MediatR; 11 | using Spectre.Console; 12 | 13 | public class ListTodosCommand : Command 14 | { 15 | public IConsole Console { get; set; } 16 | 17 | public ListTodosCommand() 18 | : base(name: "list", "Lists all todo lists in the system.") 19 | { 20 | } 21 | 22 | public new class Handler : ICommandHandler 23 | { 24 | private readonly IMediator meditor; 25 | 26 | public Handler(IMediator meditor) => 27 | this.meditor = meditor ?? throw new ArgumentNullException(nameof(meditor)); 28 | 29 | public async Task InvokeAsync(InvocationContext context) 30 | { 31 | var lists = (await this.meditor.Send(new GetTodosQuery { }))?.Lists; 32 | var table = new Table() { Title = new TableTitle($"Todo Lists ({lists?.Count})") }; 33 | _ = table.AddColumn("Id"); 34 | _ = table.AddColumn("Title"); 35 | 36 | foreach (var list in lists) 37 | { 38 | table.AddRow(list.Id.ToString(CultureInfo.InvariantCulture), list.Title); 39 | } 40 | var barchart = new BarChart() 41 | .Width(60) 42 | .Label($"[green bold underline]Summary[/]") 43 | .CenterLabel() 44 | .AddItem("Lists", lists.Count, Color.Orange1) 45 | .AddItem("Items", lists.SelectMany(l => l.Items).Count(), Color.Orange4); 46 | 47 | var grid = new Grid().Alignment(Justify.Center); 48 | grid.AddColumn(new GridColumn()); 49 | grid.AddRow(table); 50 | grid.AddRow(barchart); 51 | 52 | AnsiConsole.Render(new Panel(grid)); 53 | 54 | return 0; 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Domain/ValueObjects/Colour.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Domain.ValueObjects 2 | { 3 | using CleanCli.Todo.Domain.Common; 4 | using CleanCli.Todo.Domain.Exceptions; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | public class Colour : ValueObject 9 | { 10 | static Colour() 11 | { 12 | } 13 | 14 | private Colour() 15 | { 16 | } 17 | 18 | private Colour(string code) => this.Code = code; 19 | 20 | public static Colour From(string code) 21 | { 22 | var colour = new Colour { Code = code }; 23 | 24 | if (!SupportedColours.Contains(colour)) 25 | { 26 | throw new UnsupportedColourException(code); 27 | } 28 | 29 | return colour; 30 | } 31 | 32 | public static Colour White => new Colour("#FFFFFF"); 33 | 34 | public static Colour Red => new Colour("#FF5733"); 35 | 36 | public static Colour Orange => new Colour("#FFC300"); 37 | 38 | public static Colour Yellow => new Colour("#FFFF66"); 39 | 40 | public static Colour Green => new Colour("#CCFF99 "); 41 | 42 | public static Colour Blue => new Colour("#6666FF"); 43 | 44 | public static Colour Purple => new Colour("#9966CC"); 45 | 46 | public static Colour Grey => new Colour("#999999"); 47 | 48 | public string Code { get; private set; } 49 | 50 | public static implicit operator string(Colour colour) => colour.ToString(); 51 | 52 | public static explicit operator Colour(string code) => From(code); 53 | 54 | public override string ToString() => this.Code; 55 | 56 | protected static IEnumerable SupportedColours 57 | { 58 | get 59 | { 60 | yield return White; 61 | yield return Red; 62 | yield return Orange; 63 | yield return Yellow; 64 | yield return Green; 65 | yield return Blue; 66 | yield return Purple; 67 | yield return Grey; 68 | } 69 | } 70 | 71 | protected override IEnumerable GetEqualityComponents() 72 | { 73 | yield return this.Code; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/CleanCli.Todo.Console/Program.cs: -------------------------------------------------------------------------------- 1 | using System.CommandLine; 2 | using System.CommandLine.Builder; 3 | using System.CommandLine.Hosting; 4 | using System.CommandLine.Invocation; 5 | using System.CommandLine.Parsing; 6 | using CleanCli.Todo.Application; 7 | using CleanCli.Todo.Console; 8 | using CleanCli.Todo.Console.Commands; 9 | using CleanCli.Todo.Infrastructure; 10 | using Microsoft.Extensions.Hosting; 11 | using Serilog; 12 | 13 | var runner = BuildCommandLine() 14 | .UseHost(_ => CreateHostBuilder(args), (builder) => builder 15 | .UseEnvironment("CLI") 16 | .UseSerilog() 17 | .ConfigureServices((hostContext, services) => 18 | { 19 | services.AddSerilog(); 20 | var configuration = hostContext.Configuration; 21 | services.AddCli(); 22 | services.AddApplication(); 23 | services.AddInfrastructure(configuration); 24 | }) 25 | .UseCommandHandler() 26 | .UseCommandHandler() 27 | .UseCommandHandler() 28 | .UseCommandHandler() 29 | .UseCommandHandler() 30 | .UseCommandHandler()).UseDefaults().Build(); 31 | 32 | return await runner.InvokeAsync(args); 33 | 34 | static CommandLineBuilder BuildCommandLine() 35 | { 36 | var root = new RootCommand(); 37 | root.AddCommand(BuildTodoListCommands()); 38 | root.AddCommand(BuildTodoItemsCommands()); 39 | root.AddCommand(new MigrateCommand()); 40 | root.AddGlobalOption(new Option("--silent", "Disables diagnostics output")); 41 | root.Handler = CommandHandler.Create(() => root.Invoke("-h")); 42 | 43 | 44 | return new CommandLineBuilder(root); 45 | 46 | static Command BuildTodoListCommands() 47 | { 48 | var todolist = new Command("todolist", "Todo lists management") 49 | { 50 | new CreateTodoListCommand(), 51 | new DeleteTodoListCommand(), 52 | new GetTodoListCommand(), 53 | new ListTodosCommand(), 54 | }; 55 | return todolist; 56 | } 57 | 58 | static Command BuildTodoItemsCommands() 59 | { 60 | var todoitem = new Command("todoitem", "Todo items management") 61 | { 62 | new SeedTodoItemsCommand() 63 | }; 64 | return todoitem; 65 | } 66 | } 67 | 68 | static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args); 69 | 70 | -------------------------------------------------------------------------------- /src/Infrastructure/Persistence/ApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Infrastructure.Persistence 2 | { 3 | using CleanCli.Todo.Application.Common.Interfaces; 4 | using CleanCli.Todo.Domain.Common; 5 | using CleanCli.Todo.Domain.Entities; 6 | using Microsoft.EntityFrameworkCore; 7 | using System.Linq; 8 | using System.Reflection; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | public class ApplicationDbContext : DbContext, IApplicationDbContext 13 | { 14 | private readonly IDateTime dateTime; 15 | private readonly IDomainEventService domainEventService; 16 | 17 | public ApplicationDbContext( 18 | DbContextOptions options, 19 | IDomainEventService domainEventService, 20 | IDateTime dateTime) : base(options) 21 | { 22 | this.domainEventService = domainEventService; 23 | this.dateTime = dateTime; 24 | } 25 | 26 | public DbSet TodoItems { get; set; } 27 | 28 | public DbSet TodoLists { get; set; } 29 | 30 | public override async Task SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken()) 31 | { 32 | foreach (var entry in this.ChangeTracker.Entries()) 33 | { 34 | switch (entry.State) 35 | { 36 | case EntityState.Added: 37 | entry.Entity.Created = this.dateTime.Now; 38 | break; 39 | 40 | case EntityState.Modified: 41 | entry.Entity.LastModified = this.dateTime.Now; 42 | break; 43 | case EntityState.Detached: 44 | break; 45 | case EntityState.Unchanged: 46 | break; 47 | case EntityState.Deleted: 48 | break; 49 | default: 50 | break; 51 | } 52 | } 53 | 54 | var result = await base.SaveChangesAsync(cancellationToken); 55 | 56 | await this.DispatchEvents(); 57 | 58 | return result; 59 | } 60 | 61 | protected override void OnModelCreating(ModelBuilder modelBuilder) 62 | { 63 | modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); 64 | 65 | base.OnModelCreating(modelBuilder); 66 | } 67 | 68 | private async Task DispatchEvents() 69 | { 70 | while (true) 71 | { 72 | var domainEventEntity = this.ChangeTracker.Entries() 73 | .Select(x => x.Entity.DomainEvents) 74 | .SelectMany(x => x) 75 | .Where(domainEvent => !domainEvent.IsPublished) 76 | .FirstOrDefault(); 77 | if (domainEventEntity == null) 78 | { 79 | break; 80 | } 81 | 82 | domainEventEntity.IsPublished = true; 83 | await this.domainEventService.Publish(domainEventEntity); 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/CleanCli.Todo.Console/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace CleanCli.Todo.Console 2 | { 3 | using System; 4 | using System.CommandLine.Parsing; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using CleanCli.Todo.Application.Common.Interfaces; 8 | using MediatR; 9 | using Microsoft.Extensions.Configuration; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Serilog; 12 | using Spectre.Console; 13 | 14 | public static class ServiceCollectionExtensions 15 | { 16 | /// 17 | /// Registers CLI services. 18 | /// 19 | /// 20 | /// 21 | public static IServiceCollection AddCli(this IServiceCollection services) 22 | { 23 | services.AddTransient(typeof(IPipelineBehavior<,>), typeof(UnhandledExceptionBehaviour<,>)); 24 | services.AddSingleton(); 25 | return services; 26 | } 27 | 28 | /// 29 | /// Registers Serilog from configuration. 30 | /// 31 | /// 32 | /// 33 | public static IServiceCollection AddSerilog(this IServiceCollection services) 34 | { 35 | Log.Logger = CreateLogger(services); 36 | return services; 37 | } 38 | private static Serilog.Core.Logger CreateLogger(IServiceCollection services) 39 | { 40 | var scope = services.BuildServiceProvider(); 41 | var parseResult = scope.GetRequiredService(); 42 | var isSilentLogger = parseResult.ValueForOption("--silent"); 43 | var loggerConfiguration = new LoggerConfiguration() 44 | .ReadFrom.Configuration(scope.GetRequiredService()); 45 | 46 | if (isSilentLogger) 47 | { 48 | loggerConfiguration.MinimumLevel.Override("CleanCli", Serilog.Events.LogEventLevel.Warning); 49 | loggerConfiguration.MinimumLevel.Override("Microsoft", Serilog.Events.LogEventLevel.Warning); 50 | } 51 | 52 | return loggerConfiguration.CreateLogger(); 53 | } 54 | 55 | public class UserServiceStub : ICurrentUserService 56 | { 57 | public string UserId => "admin@cli"; 58 | } 59 | 60 | /// 61 | /// NOTE: Pipeline behavior registration order is important. 62 | /// 63 | /// 64 | /// 65 | public class UnhandledExceptionBehaviour : IPipelineBehavior 66 | { 67 | public async Task Handle( 68 | TRequest request, 69 | CancellationToken cancellationToken, 70 | RequestHandlerDelegate next) 71 | { 72 | try 73 | { 74 | return await next(); 75 | } 76 | catch (Exception ex) 77 | { 78 | AnsiConsole.WriteException(ex); 79 | return default; 80 | } 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /CleanCli.Todo.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30907.101 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanCli.Todo.Console", "src\CleanCli.Todo.Console\CleanCli.Todo.Console.csproj", "{AF34CA85-E5F1-48E0-A070-D16E052E4487}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A99C9EB5-6B20-4436-9FA6-FE56BAD88E9D}" 9 | ProjectSection(SolutionItems) = preProject 10 | .editorconfig = .editorconfig 11 | .gitattributes = .gitattributes 12 | .gitignore = .gitignore 13 | build.cake = build.cake 14 | Directory.Build.props = Directory.Build.props 15 | README.md = README.md 16 | EndProjectSection 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{BE2F5558-E8DE-4866-A672-BAF2C726319F}" 19 | EndProject 20 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{73FD069A-067D-44B9-B9C6-CB03638712F2}" 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Domain", "src\Domain\Domain.csproj", "{F6F5208B-EDD7-4086-B875-B9E0CFEDD0F4}" 23 | EndProject 24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Application", "src\Application\Application.csproj", "{580534D7-BD8B-4186-A308-210579DA2D9F}" 25 | EndProject 26 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Infrastructure", "src\Infrastructure\Infrastructure.csproj", "{1B834FE2-C261-4DEC-A2C9-789AC0B5C4D0}" 27 | EndProject 28 | Global 29 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 30 | Debug|Any CPU = Debug|Any CPU 31 | Release|Any CPU = Release|Any CPU 32 | EndGlobalSection 33 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 34 | {AF34CA85-E5F1-48E0-A070-D16E052E4487}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {AF34CA85-E5F1-48E0-A070-D16E052E4487}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {AF34CA85-E5F1-48E0-A070-D16E052E4487}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {AF34CA85-E5F1-48E0-A070-D16E052E4487}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {F6F5208B-EDD7-4086-B875-B9E0CFEDD0F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {F6F5208B-EDD7-4086-B875-B9E0CFEDD0F4}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {F6F5208B-EDD7-4086-B875-B9E0CFEDD0F4}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {F6F5208B-EDD7-4086-B875-B9E0CFEDD0F4}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {580534D7-BD8B-4186-A308-210579DA2D9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {580534D7-BD8B-4186-A308-210579DA2D9F}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {580534D7-BD8B-4186-A308-210579DA2D9F}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {580534D7-BD8B-4186-A308-210579DA2D9F}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {1B834FE2-C261-4DEC-A2C9-789AC0B5C4D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {1B834FE2-C261-4DEC-A2C9-789AC0B5C4D0}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {1B834FE2-C261-4DEC-A2C9-789AC0B5C4D0}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {1B834FE2-C261-4DEC-A2C9-789AC0B5C4D0}.Release|Any CPU.Build.0 = Release|Any CPU 50 | EndGlobalSection 51 | GlobalSection(SolutionProperties) = preSolution 52 | HideSolutionNode = FALSE 53 | EndGlobalSection 54 | GlobalSection(NestedProjects) = preSolution 55 | {AF34CA85-E5F1-48E0-A070-D16E052E4487} = {BE2F5558-E8DE-4866-A672-BAF2C726319F} 56 | {F6F5208B-EDD7-4086-B875-B9E0CFEDD0F4} = {BE2F5558-E8DE-4866-A672-BAF2C726319F} 57 | {580534D7-BD8B-4186-A308-210579DA2D9F} = {BE2F5558-E8DE-4866-A672-BAF2C726319F} 58 | {1B834FE2-C261-4DEC-A2C9-789AC0B5C4D0} = {BE2F5558-E8DE-4866-A672-BAF2C726319F} 59 | EndGlobalSection 60 | GlobalSection(ExtensibilityGlobals) = postSolution 61 | SolutionGuid = {A8A1BFCA-C535-44B7-A594-9430DB066C77} 62 | EndGlobalSection 63 | EndGlobal 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CleanCli.Todo.Example 2 | 3 | ![GitHub Actions Status](https://github.com/nikiforovall/clean-cli-todo-example/workflows/Build/badge.svg?branch=main) 4 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) 5 | 6 | [![GitHub Actions Build History](https://buildstats.info/github/chart/nikiforovall/clean-cli-todo-example?branch=main&includeBuildsFromPullRequest=false)](https://github.com/nikiforovall/clean-cli-todo-example/actions) 7 | 8 | An example of how to use Clean Architecture together with CLI applications. 9 | 10 | ## Goals 11 | 12 | * To use a CLI as UI for a Clean Architecture based solutions. As result, you can use CLI as standalone implementation or just auxiliary project to perform a project tasks. 13 | * To design CLI application that clearly communicates implemented functionality (commands) and structural composition in general. 14 | * To provide first class user experience through CLI implementation best practices. Luckily, `System.CommandLine` help with things such as "help text" generation and autocompletion. 15 | 16 | ```csharp 17 | var runner = BuildCommandLine() 18 | .UseHost(_ => Host.CreateDefaultBuilder(args), (builder) => 19 | { 20 | builder.UseEnvironment("CLI") 21 | .UseSerilog() 22 | .ConfigureServices((hostContext, services) => 23 | { 24 | services.AddSerilog(); 25 | }) 26 | .UseCommandHandler() 27 | .UseCommandHandler() 28 | .UseCommandHandler() 29 | .UseCommandHandler() 30 | .UseCommandHandler(); 31 | }).UseDefaults().Build(); 32 | 33 | return await runner.InvokeAsync(args); 34 | ``` 35 | 36 | ## Technologies 37 | 38 | | Dependencies | Description | 39 | |----------------------------------------------|--------------------------------------------------------| 40 | | | Command execution, provides command/handler app model. | 41 | | | Beautiful console apps | 42 | 43 | The application is based on popular template. 44 | 45 | ## Commands Overview 46 | 47 | Top level commands: 48 | 49 | ```bash 50 | $ dotnet run -- -h 51 | CleanCli.Todo.Console 52 | 53 | Usage: 54 | CleanCli.Todo.Console [options] [command] 55 | 56 | Options: 57 | --silent Disables diagnostics output 58 | --version Show version information 59 | -?, -h, --help Show help and usage information 60 | 61 | Commands: 62 | todolist Todo lists management 63 | todoitem Todo items management 64 | ``` 65 | 66 | Todo List commands: 67 | 68 | ```bash 69 | $ dotnet run -- todolist -h 70 | todolist 71 | Todo lists management 72 | 73 | Usage: 74 | CleanCli.Todo.Console [options] todolist [command] 75 | 76 | Options: 77 | --silent Disables diagnostics output 78 | -?, -h, --help Show help and usage information 79 | 80 | Commands: 81 | create Creates todo list 82 | delete Deletes todo list 83 | get Gets a todo list 84 | list Lists all todo lists in the system. 85 | 86 | # command details example 87 | $ dotnet run -- todolist create -h 88 | create 89 | Creates todo list 90 | 91 | Usage: 92 | CleanCli.Todo.Console [options] todolist create 93 | 94 | Options: 95 | -t, --title Title of the todo list 96 | --dry-run Displays a summary of what would happen if the given command line were run. 97 | --silent Disables diagnostics output 98 | -?, -h, --help Show help and usage information 99 | ``` 100 | 101 | Todo Items commands: 102 | 103 | ```bash 104 | $ dotnet run -- todoitem -h 105 | todoitem 106 | Todo items management 107 | 108 | Usage: 109 | CleanCli.Todo.Console [options] todoitem [command] 110 | 111 | Options: 112 | --silent Disables diagnostics output 113 | -?, -h, --help Show help and usage information 114 | 115 | Commands: 116 | seed Seeds a random todo items. 117 | 118 | # command details example 119 | $ dotnet run -- todoitem seed -h 120 | seed 121 | Seeds a random todo items. 122 | 123 | Usage: 124 | CleanCli.Todo.Console [options] todoitem seed 125 | 126 | Options: 127 | -l, --todolist <todolist> Title of the todo list 128 | -n, --number <number> The number of todo items to be generated. [default: 3] 129 | --silent Disables diagnostics output 130 | -?, -h, --help Show help and usage information 131 | ``` 132 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | 33 | # Visual Studio 2015/2017 cache/options directory 34 | .vs/ 35 | # Uncomment if you have tasks that create the project's static files in wwwroot 36 | #wwwroot/ 37 | 38 | # Visual Studio 2017 auto generated files 39 | Generated\ Files/ 40 | 41 | # MSTest test Results 42 | [Tt]est[Rr]esult*/ 43 | [Bb]uild[Ll]og.* 44 | 45 | # NUnit 46 | *.VisualState.xml 47 | TestResult.xml 48 | nunit-*.xml 49 | 50 | # Build Results of an ATL Project 51 | [Dd]ebugPS/ 52 | [Rr]eleasePS/ 53 | dlldata.c 54 | 55 | # Benchmark Results 56 | BenchmarkDotNet.Artifacts/ 57 | 58 | # .NET Core 59 | project.lock.json 60 | project.fragment.lock.json 61 | artifacts/ 62 | 63 | # StyleCop 64 | StyleCopReport.xml 65 | 66 | # Files built by Visual Studio 67 | *_i.c 68 | *_p.c 69 | *_h.h 70 | *.ilk 71 | *.meta 72 | *.obj 73 | *.iobj 74 | *.pch 75 | *.pdb 76 | *.ipdb 77 | *.pgc 78 | *.pgd 79 | *.rsp 80 | *.sbr 81 | *.tlb 82 | *.tli 83 | *.tlh 84 | *.tmp 85 | *.tmp_proj 86 | *_wpftmp.csproj 87 | *.log 88 | *.vspscc 89 | *.vssscc 90 | .builds 91 | *.pidb 92 | *.svclog 93 | *.scc 94 | 95 | # Chutzpah Test files 96 | _Chutzpah* 97 | 98 | # Visual C++ cache files 99 | ipch/ 100 | *.aps 101 | *.ncb 102 | *.opendb 103 | *.opensdf 104 | *.sdf 105 | *.cachefile 106 | *.VC.db 107 | *.VC.VC.opendb 108 | 109 | # Visual Studio profiler 110 | *.psess 111 | *.vsp 112 | *.vspx 113 | *.sap 114 | 115 | # Visual Studio Trace Files 116 | *.e2e 117 | 118 | # TFS 2012 Local Workspace 119 | $tf/ 120 | 121 | # Guidance Automation Toolkit 122 | *.gpState 123 | 124 | # ReSharper is a .NET coding add-in 125 | _ReSharper*/ 126 | *.[Rr]e[Ss]harper 127 | *.DotSettings.user 128 | 129 | # JustCode is a .NET coding add-in 130 | .JustCode 131 | 132 | # TeamCity is a build add-in 133 | _TeamCity* 134 | 135 | # DotCover is a Code Coverage Tool 136 | *.dotCover 137 | 138 | # AxoCover is a Code Coverage Tool 139 | .axoCover/* 140 | !.axoCover/settings.json 141 | 142 | # Visual Studio code coverage results 143 | *.coverage 144 | *.coveragexml 145 | 146 | # NCrunch 147 | _NCrunch_* 148 | .*crunch*.local.xml 149 | nCrunchTemp_* 150 | 151 | # MightyMoose 152 | *.mm.* 153 | AutoTest.Net/ 154 | 155 | # Web workbench (sass) 156 | .sass-cache/ 157 | 158 | # Installshield output folder 159 | [Ee]xpress/ 160 | 161 | # DocProject is a documentation generator add-in 162 | DocProject/buildhelp/ 163 | DocProject/Help/*.HxT 164 | DocProject/Help/*.HxC 165 | DocProject/Help/*.hhc 166 | DocProject/Help/*.hhk 167 | DocProject/Help/*.hhp 168 | DocProject/Help/Html2 169 | DocProject/Help/html 170 | 171 | # Click-Once directory 172 | publish/ 173 | 174 | # Publish Web Output 175 | *.[Pp]ublish.xml 176 | *.azurePubxml 177 | # Note: Comment the next line if you want to checkin your web deploy settings, 178 | # but database connection strings (with potential passwords) will be unencrypted 179 | *.pubxml 180 | *.publishproj 181 | 182 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 183 | # checkin your Azure Web App publish settings, but sensitive information contained 184 | # in these scripts will be unencrypted 185 | PublishScripts/ 186 | 187 | # NuGet Packages 188 | *.nupkg 189 | # NuGet Symbol Packages 190 | *.snupkg 191 | # The packages folder can be ignored because of Package Restore 192 | **/[Pp]ackages/* 193 | # except build/, which is used as an MSBuild target. 194 | !**/[Pp]ackages/build/ 195 | # Uncomment if necessary however generally it will be regenerated when needed 196 | #!**/[Pp]ackages/repositories.config 197 | # NuGet v3's project.json files produces more ignorable files 198 | *.nuget.props 199 | *.nuget.targets 200 | 201 | # Microsoft Azure Build Output 202 | csx/ 203 | *.build.csdef 204 | 205 | # Microsoft Azure Emulator 206 | ecf/ 207 | rcf/ 208 | 209 | # Windows Store app package directories and files 210 | AppPackages/ 211 | BundleArtifacts/ 212 | Package.StoreAssociation.xml 213 | _pkginfo.txt 214 | *.appx 215 | *.appxbundle 216 | *.appxupload 217 | 218 | # Visual Studio cache files 219 | # files ending in .cache can be ignored 220 | *.[Cc]ache 221 | # but keep track of directories ending in .cache 222 | !?*.[Cc]ache/ 223 | 224 | # Others 225 | ClientBin/ 226 | ~$* 227 | *~ 228 | *.dbmdl 229 | *.dbproj.schemaview 230 | *.jfm 231 | *.pfx 232 | *.publishsettings 233 | orleans.codegen.cs 234 | 235 | # Including strong name files can present a security risk 236 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 237 | #*.snk 238 | 239 | # Since there are multiple workflows, uncomment next line to ignore bower_components 240 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 241 | #bower_components/ 242 | 243 | # RIA/Silverlight projects 244 | Generated_Code/ 245 | 246 | # Backup & report files from converting an old project file 247 | # to a newer Visual Studio version. Backup files are not needed, 248 | # because we have git ;-) 249 | _UpgradeReport_Files/ 250 | Backup*/ 251 | UpgradeLog*.XML 252 | UpgradeLog*.htm 253 | ServiceFabricBackup/ 254 | *.rptproj.bak 255 | 256 | # SQL Server files 257 | *.mdf 258 | *.ldf 259 | *.ndf 260 | 261 | # Business Intelligence projects 262 | *.rdl.data 263 | *.bim.layout 264 | *.bim_*.settings 265 | *.rptproj.rsuser 266 | *- [Bb]ackup.rdl 267 | *- [Bb]ackup ([0-9]).rdl 268 | *- [Bb]ackup ([0-9][0-9]).rdl 269 | 270 | # Microsoft Fakes 271 | FakesAssemblies/ 272 | 273 | # GhostDoc plugin setting file 274 | *.GhostDoc.xml 275 | 276 | # Node.js Tools for Visual Studio 277 | .ntvs_analysis.dat 278 | node_modules/ 279 | 280 | # Visual Studio 6 build log 281 | *.plg 282 | 283 | # Visual Studio 6 workspace options file 284 | *.opt 285 | 286 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 287 | *.vbw 288 | 289 | # Visual Studio LightSwitch build output 290 | **/*.HTMLClient/GeneratedArtifacts 291 | **/*.DesktopClient/GeneratedArtifacts 292 | **/*.DesktopClient/ModelManifest.xml 293 | **/*.Server/GeneratedArtifacts 294 | **/*.Server/ModelManifest.xml 295 | _Pvt_Extensions 296 | 297 | # Paket dependency manager 298 | .paket/paket.exe 299 | paket-files/ 300 | 301 | # FAKE - F# Make 302 | .fake/ 303 | 304 | # CodeRush personal settings 305 | .cr/personal 306 | 307 | # Python Tools for Visual Studio (PTVS) 308 | __pycache__/ 309 | *.pyc 310 | 311 | # Cake - Uncomment if you are using it 312 | # tools/** 313 | # !tools/packages.config 314 | 315 | # Tabs Studio 316 | *.tss 317 | 318 | # Telerik's JustMock configuration file 319 | *.jmconfig 320 | 321 | # BizTalk build output 322 | *.btp.cs 323 | *.btm.cs 324 | *.odx.cs 325 | *.xsd.cs 326 | 327 | # OpenCover UI analysis results 328 | OpenCover/ 329 | 330 | # Azure Stream Analytics local run output 331 | ASALocalRun/ 332 | 333 | # MSBuild Binary and Structured Log 334 | *.binlog 335 | 336 | # NVidia Nsight GPU debugger configuration file 337 | *.nvuser 338 | 339 | # MFractors (Xamarin productivity tool) working folder 340 | .mfractor/ 341 | 342 | # Local History for Visual Studio 343 | .localhistory/ 344 | 345 | # BeatPulse healthcheck temp database 346 | healthchecksdb 347 | 348 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 349 | MigrationBackup/ 350 | 351 | # Ionide (cross platform F# VS Code tools) working folder 352 | .ionide/ 353 | 354 | Artefacts/ 355 | 356 | .idea 357 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ########################################## 2 | # Common Settings 3 | ########################################## 4 | 5 | # This file is the top-most EditorConfig file 6 | root = true 7 | 8 | # All Files 9 | [*] 10 | charset = utf-8 11 | indent_style = space 12 | indent_size = 4 13 | insert_final_newline = true 14 | trim_trailing_whitespace = true 15 | 16 | ########################################## 17 | # File Extension Settings 18 | ########################################## 19 | 20 | # Visual Studio Solution Files 21 | [*.sln] 22 | indent_style = tab 23 | 24 | # Visual Studio XML Project Files 25 | [*.{csproj,vbproj,vcxproj.filters,proj,projitems,shproj}] 26 | indent_size = 2 27 | 28 | # XML Configuration Files 29 | [*.{xml,config,props,targets,nuspec,resx,ruleset,vsixmanifest,vsct}] 30 | indent_size = 2 31 | 32 | # JSON Files 33 | [*.{json,json5,webmanifest}] 34 | indent_size = 2 35 | 36 | # YAML Files 37 | [*.{yml,yaml}] 38 | indent_size = 2 39 | 40 | # Markdown Files 41 | [*.md] 42 | trim_trailing_whitespace = false 43 | 44 | # Web Files 45 | [*.{htm,html,js,jsm,ts,tsx,css,sass,scss,less,svg,vue}] 46 | indent_size = 2 47 | 48 | # Batch Files 49 | [*.{cmd,bat}] 50 | end_of_line = crlf 51 | 52 | # Bash Files 53 | [*.sh] 54 | end_of_line = lf 55 | 56 | # Makefiles 57 | [Makefile] 58 | indent_style = tab 59 | 60 | ########################################## 61 | # Default .NET Code Style Severities 62 | # https://docs.microsoft.com/dotnet/fundamentals/code-analysis/configuration-options#scope 63 | ########################################## 64 | 65 | [*.{cs,csx,cake,vb,vbx}] 66 | # Default Severity for all .NET Code Style rules below 67 | dotnet_analyzer_diagnostic.severity = warning 68 | 69 | ########################################## 70 | # TODO: this is already present in this file and section below is inteded to force this on editor level. 71 | # IDE Code Style IDE_XXXX 72 | # Motivation https://github.com/dotnet/roslyn/blob/9f87b444da9c48a4d492b19f8337339056bf2b95/src/Analyzers/Core/Analyzers/EnforceOnBuildValues.cs 73 | ########################################## 74 | # IDE0055 Fix formatting 75 | dotnet_diagnostic.IDE0055.severity = error 76 | # IDE005_gen: Remove unnecessary usings in generated code 77 | dotnet_diagnostic.IDE0005.severity = error 78 | # IDE0065: Using directives must be placed outside of a namespace declaration 79 | dotnet_diagnostic.IDE0065.severity = error 80 | # IDE0059: Unnecessary assignment 81 | dotnet_diagnostic.IDE0059.severity = error 82 | # IDE0007 UseImplicitType 83 | dotnet_diagnostic.IDE0007.severity = error 84 | # IDE0008 85 | dotnet_diagnostic.IDE0008.severity = error 86 | # IDE0011 UseExplicitType 87 | dotnet_diagnostic.IDE0011.severity = error 88 | # IDE0040 AddAccessibilityModifiers 89 | dotnet_diagnostic.IDE0040.severity = error 90 | # IDE0060 UnusedParameter 91 | dotnet_diagnostic.IDE0060.severity = error 92 | # IDE0036 Order modifiers 93 | dotnet_diagnostic.IDE0036.severity = error 94 | # IDE0059 Remove unnecessary value assignment 95 | dotnet_diagnostic.IDE0059.severity = error 96 | # IDE0016 Use throw expression 97 | dotnet_diagnostic.IDE0016.severity = suggestion 98 | 99 | ########################################## 100 | # File Header (Uncomment to support file headers) 101 | # https://docs.microsoft.com/visualstudio/ide/reference/add-file-header 102 | ########################################## 103 | 104 | # IDE0073: The file header is missing or not located at the top of the file 105 | dotnet_diagnostic.IDE0073.severity = error 106 | 107 | ########################################## 108 | # .NET Language Conventions 109 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions 110 | ########################################## 111 | 112 | # .NET Code Style Settings 113 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#net-code-style-settings 114 | [*.{cs,csx,cake,vb,vbx}] 115 | 116 | # "this." and "Me." qualifiers 117 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#this-and-me 118 | dotnet_style_qualification_for_field = true:warning 119 | dotnet_style_qualification_for_property = true:warning 120 | dotnet_style_qualification_for_method = true:warning 121 | dotnet_style_qualification_for_event = true:warning 122 | # Language keywords instead of framework type names for type references 123 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#language-keywords 124 | dotnet_style_predefined_type_for_locals_parameters_members = true:warning 125 | dotnet_style_predefined_type_for_member_access = true:warning 126 | # Modifier preferences 127 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#normalize-modifiers 128 | dotnet_style_require_accessibility_modifiers = always:warning 129 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:warning 130 | visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:warning 131 | dotnet_style_readonly_field = true:warning 132 | # Parentheses preferences 133 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#parentheses-preferences 134 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning 135 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning 136 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning 137 | dotnet_style_parentheses_in_other_operators = always_for_clarity:suggestion 138 | # Expression-level preferences 139 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#expression-level-preferences 140 | dotnet_style_object_initializer = true:warning 141 | dotnet_style_collection_initializer = true:warning 142 | dotnet_style_explicit_tuple_names = true:warning 143 | dotnet_style_prefer_inferred_tuple_names = true:warning 144 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning 145 | dotnet_style_prefer_auto_properties = true:warning 146 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning 147 | dotnet_style_prefer_conditional_expression_over_assignment = false:suggestion 148 | dotnet_diagnostic.IDE0045.severity = suggestion 149 | dotnet_style_prefer_conditional_expression_over_return = false:suggestion 150 | dotnet_diagnostic.IDE0046.severity = suggestion 151 | dotnet_style_prefer_compound_assignment = true:warning 152 | # Null-checking preferences 153 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#null-checking-preferences 154 | dotnet_style_coalesce_expression = true:warning 155 | dotnet_style_null_propagation = true:warning 156 | # Parameter preferences 157 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#parameter-preferences 158 | dotnet_code_quality_unused_parameters = all:warning 159 | # More style options (Undocumented) 160 | # https://github.com/MicrosoftDocs/visualstudio-docs/issues/3641 161 | dotnet_style_operator_placement_when_wrapping = end_of_line 162 | # https://github.com/dotnet/roslyn/pull/40070 163 | dotnet_style_prefer_simplified_interpolation = true:warning 164 | 165 | # C# Code Style Settings 166 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#c-code-style-settings 167 | [*.{cs,csx,cake}] 168 | # Implicit and explicit types 169 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#implicit-and-explicit-types 170 | csharp_style_var_for_built_in_types = true:warning 171 | csharp_style_var_when_type_is_apparent = true:warning 172 | csharp_style_var_elsewhere = true:warning 173 | # Expression-bodied members 174 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#expression-bodied-members 175 | csharp_style_expression_bodied_methods = true:warning 176 | csharp_style_expression_bodied_constructors = true:warning 177 | csharp_style_expression_bodied_operators = true:warning 178 | csharp_style_expression_bodied_properties = true:warning 179 | csharp_style_expression_bodied_indexers = true:warning 180 | csharp_style_expression_bodied_accessors = true:warning 181 | csharp_style_expression_bodied_lambdas = true:warning 182 | csharp_style_expression_bodied_local_functions = true:warning 183 | # Pattern matching 184 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#pattern-matching 185 | csharp_style_pattern_matching_over_is_with_cast_check = true:warning 186 | csharp_style_pattern_matching_over_as_with_null_check = true:warning 187 | # Inlined variable declarations 188 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#inlined-variable-declarations 189 | csharp_style_inlined_variable_declaration = true:warning 190 | # Expression-level preferences 191 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#expression-level-preferences 192 | csharp_prefer_simple_default_expression = true:warning 193 | # "Null" checking preferences 194 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#c-null-checking-preferences 195 | csharp_style_throw_expression = true:warning 196 | csharp_style_conditional_delegate_call = true:warning 197 | # Code block preferences 198 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#code-block-preferences 199 | csharp_prefer_braces = true:warning 200 | # Unused value preferences 201 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#unused-value-preferences 202 | csharp_style_unused_value_expression_statement_preference = discard_variable:suggestion 203 | dotnet_diagnostic.IDE0058.severity = suggestion 204 | csharp_style_unused_value_assignment_preference = discard_variable:suggestion 205 | dotnet_diagnostic.IDE0059.severity = suggestion 206 | # Index and range preferences 207 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#index-and-range-preferences 208 | csharp_style_prefer_index_operator = true:warning 209 | csharp_style_prefer_range_operator = true:warning 210 | # Miscellaneous preferences 211 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#miscellaneous-preferences 212 | csharp_style_deconstructed_variable_declaration = true:warning 213 | csharp_style_pattern_local_over_anonymous_function = true:warning 214 | csharp_using_directive_placement = inside_namespace:warning 215 | csharp_prefer_static_local_function = true:warning 216 | csharp_prefer_simple_using_statement = true:suggestion 217 | dotnet_diagnostic.IDE0063.severity = suggestion 218 | 219 | ########################################## 220 | # .NET Formatting Conventions 221 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-code-style-settings-reference#formatting-conventions 222 | ########################################## 223 | 224 | # Organize usings 225 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#organize-using-directives 226 | dotnet_sort_system_directives_first = true 227 | # Newline options 228 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#new-line-options 229 | csharp_new_line_before_open_brace = all 230 | csharp_new_line_before_else = true 231 | csharp_new_line_before_catch = true 232 | csharp_new_line_before_finally = true 233 | csharp_new_line_before_members_in_object_initializers = true 234 | csharp_new_line_before_members_in_anonymous_types = true 235 | csharp_new_line_between_query_expression_clauses = true 236 | # Indentation options 237 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#indentation-options 238 | csharp_indent_case_contents = true 239 | csharp_indent_switch_labels = true 240 | csharp_indent_labels = no_change 241 | csharp_indent_block_contents = true 242 | csharp_indent_braces = false 243 | csharp_indent_case_contents_when_block = false 244 | # Spacing options 245 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#spacing-options 246 | csharp_space_after_cast = false 247 | csharp_space_after_keywords_in_control_flow_statements = true 248 | csharp_space_between_parentheses = false 249 | csharp_space_before_colon_in_inheritance_clause = true 250 | csharp_space_after_colon_in_inheritance_clause = true 251 | csharp_space_around_binary_operators = before_and_after 252 | csharp_space_between_method_declaration_parameter_list_parentheses = false 253 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 254 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 255 | csharp_space_between_method_call_parameter_list_parentheses = false 256 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 257 | csharp_space_between_method_call_name_and_opening_parenthesis = false 258 | csharp_space_after_comma = true 259 | csharp_space_before_comma = false 260 | csharp_space_after_dot = false 261 | csharp_space_before_dot = false 262 | csharp_space_after_semicolon_in_for_statement = true 263 | csharp_space_before_semicolon_in_for_statement = false 264 | csharp_space_around_declaration_statements = false 265 | csharp_space_before_open_square_brackets = false 266 | csharp_space_between_empty_square_brackets = false 267 | csharp_space_between_square_brackets = false 268 | # Wrapping options 269 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#wrap-options 270 | csharp_preserve_single_line_statements = false 271 | csharp_preserve_single_line_blocks = true 272 | 273 | ########################################## 274 | # .NET Naming Conventions 275 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-naming-conventions 276 | ########################################## 277 | 278 | [*.{cs,csx,cake,vb,vbx}] 279 | 280 | ########################################## 281 | # Styles 282 | ########################################## 283 | 284 | # camel_case_style - Define the camelCase style 285 | dotnet_naming_style.camel_case_style.capitalization = camel_case 286 | # pascal_case_style - Define the PascalCase style 287 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 288 | # first_upper_style - The first character must start with an upper-case character 289 | dotnet_naming_style.first_upper_style.capitalization = first_word_upper 290 | # prefix_interface_with_i_style - Interfaces must be PascalCase and the first character of an interface must be an 'I' 291 | dotnet_naming_style.prefix_interface_with_i_style.capitalization = pascal_case 292 | dotnet_naming_style.prefix_interface_with_i_style.required_prefix = I 293 | # prefix_type_parameters_with_t_style - Generic Type Parameters must be PascalCase and the first character must be a 'T' 294 | dotnet_naming_style.prefix_type_parameters_with_t_style.capitalization = pascal_case 295 | dotnet_naming_style.prefix_type_parameters_with_t_style.required_prefix = T 296 | # disallowed_style - Anything that has this style applied is marked as disallowed 297 | dotnet_naming_style.disallowed_style.capitalization = pascal_case 298 | dotnet_naming_style.disallowed_style.required_prefix = ____RULE_VIOLATION____ 299 | dotnet_naming_style.disallowed_style.required_suffix = ____RULE_VIOLATION____ 300 | # internal_error_style - This style should never occur... if it does, it indicates a bug in file or in the parser using the file 301 | dotnet_naming_style.internal_error_style.capitalization = pascal_case 302 | dotnet_naming_style.internal_error_style.required_prefix = ____INTERNAL_ERROR____ 303 | dotnet_naming_style.internal_error_style.required_suffix = ____INTERNAL_ERROR____ 304 | 305 | ########################################## 306 | # .NET Design Guideline Field Naming Rules 307 | # Naming rules for fields follow the .NET Framework design guidelines 308 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/index 309 | ########################################## 310 | 311 | # All public/protected/protected_internal constant fields must be PascalCase 312 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/field 313 | dotnet_naming_symbols.public_protected_constant_fields_group.applicable_accessibilities = public, protected, protected_internal 314 | dotnet_naming_symbols.public_protected_constant_fields_group.required_modifiers = const 315 | dotnet_naming_symbols.public_protected_constant_fields_group.applicable_kinds = field 316 | dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.symbols = public_protected_constant_fields_group 317 | dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.style = pascal_case_style 318 | dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.severity = warning 319 | 320 | # All public/protected/protected_internal static readonly fields must be PascalCase 321 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/field 322 | dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_accessibilities = public, protected, protected_internal 323 | dotnet_naming_symbols.public_protected_static_readonly_fields_group.required_modifiers = static, readonly 324 | dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_kinds = field 325 | dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.symbols = public_protected_static_readonly_fields_group 326 | dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.style = pascal_case_style 327 | dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.severity = warning 328 | 329 | # No other public/protected/protected_internal fields are allowed 330 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/field 331 | dotnet_naming_symbols.other_public_protected_fields_group.applicable_accessibilities = public, protected, protected_internal 332 | dotnet_naming_symbols.other_public_protected_fields_group.applicable_kinds = field 333 | dotnet_naming_rule.other_public_protected_fields_disallowed_rule.symbols = other_public_protected_fields_group 334 | dotnet_naming_rule.other_public_protected_fields_disallowed_rule.style = disallowed_style 335 | dotnet_naming_rule.other_public_protected_fields_disallowed_rule.severity = error 336 | 337 | ########################################## 338 | # StyleCop Field Naming Rules 339 | # Naming rules for fields follow the StyleCop analyzers 340 | # This does not override any rules using disallowed_style above 341 | # https://github.com/DotNetAnalyzers/StyleCopAnalyzers 342 | ########################################## 343 | 344 | # All constant fields must be PascalCase 345 | # https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1303.md 346 | dotnet_naming_symbols.stylecop_constant_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private 347 | dotnet_naming_symbols.stylecop_constant_fields_group.required_modifiers = const 348 | dotnet_naming_symbols.stylecop_constant_fields_group.applicable_kinds = field 349 | dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.symbols = stylecop_constant_fields_group 350 | dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.style = pascal_case_style 351 | dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.severity = warning 352 | 353 | # All static readonly fields must be PascalCase 354 | # https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1311.md 355 | dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private 356 | dotnet_naming_symbols.stylecop_static_readonly_fields_group.required_modifiers = static, readonly 357 | dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_kinds = field 358 | dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.symbols = stylecop_static_readonly_fields_group 359 | dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.style = pascal_case_style 360 | dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.severity = warning 361 | 362 | # No non-private instance fields are allowed 363 | # https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1401.md 364 | dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected 365 | dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_kinds = field 366 | dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.symbols = stylecop_fields_must_be_private_group 367 | dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.style = disallowed_style 368 | dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.severity = error 369 | 370 | # Private fields must be camelCase 371 | # https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1306.md 372 | dotnet_naming_symbols.stylecop_private_fields_group.applicable_accessibilities = private 373 | dotnet_naming_symbols.stylecop_private_fields_group.applicable_kinds = field 374 | dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.symbols = stylecop_private_fields_group 375 | dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.style = camel_case_style 376 | dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.severity = warning 377 | 378 | # Local variables must be camelCase 379 | # https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1312.md 380 | dotnet_naming_symbols.stylecop_local_fields_group.applicable_accessibilities = local 381 | dotnet_naming_symbols.stylecop_local_fields_group.applicable_kinds = local 382 | dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.symbols = stylecop_local_fields_group 383 | dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.style = camel_case_style 384 | dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.severity = silent 385 | 386 | # This rule should never fire. However, it's included for at least two purposes: 387 | # First, it helps to understand, reason about, and root-case certain types of issues, such as bugs in .editorconfig parsers. 388 | # Second, it helps to raise immediate awareness if a new field type is added (as occurred recently in C#). 389 | dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_accessibilities = * 390 | dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_kinds = field 391 | dotnet_naming_rule.sanity_check_uncovered_field_case_rule.symbols = sanity_check_uncovered_field_case_group 392 | dotnet_naming_rule.sanity_check_uncovered_field_case_rule.style = internal_error_style 393 | dotnet_naming_rule.sanity_check_uncovered_field_case_rule.severity = error 394 | 395 | 396 | ########################################## 397 | # Other Naming Rules 398 | ########################################## 399 | 400 | # All of the following must be PascalCase: 401 | # - Namespaces 402 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-namespaces 403 | # https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md 404 | # - Classes and Enumerations 405 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces 406 | # https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md 407 | # - Delegates 408 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces#names-of-common-types 409 | # - Constructors, Properties, Events, Methods 410 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-type-members 411 | dotnet_naming_symbols.element_group.applicable_kinds = namespace, class, enum, struct, delegate, event, method, property 412 | dotnet_naming_rule.element_rule.symbols = element_group 413 | dotnet_naming_rule.element_rule.style = pascal_case_style 414 | dotnet_naming_rule.element_rule.severity = warning 415 | 416 | # Interfaces use PascalCase and are prefixed with uppercase 'I' 417 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces 418 | dotnet_naming_symbols.interface_group.applicable_kinds = interface 419 | dotnet_naming_rule.interface_rule.symbols = interface_group 420 | dotnet_naming_rule.interface_rule.style = prefix_interface_with_i_style 421 | dotnet_naming_rule.interface_rule.severity = warning 422 | 423 | # Generics Type Parameters use PascalCase and are prefixed with uppercase 'T' 424 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces 425 | dotnet_naming_symbols.type_parameter_group.applicable_kinds = type_parameter 426 | dotnet_naming_rule.type_parameter_rule.symbols = type_parameter_group 427 | dotnet_naming_rule.type_parameter_rule.style = prefix_type_parameters_with_t_style 428 | dotnet_naming_rule.type_parameter_rule.severity = warning 429 | 430 | # Function parameters use camelCase 431 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/naming-parameters 432 | dotnet_naming_symbols.parameters_group.applicable_kinds = parameter 433 | dotnet_naming_rule.parameters_rule.symbols = parameters_group 434 | dotnet_naming_rule.parameters_rule.style = camel_case_style 435 | dotnet_naming_rule.parameters_rule.severity = warning 436 | --------------------------------------------------------------------------------