├── nginx ├── Dockerfile └── app.conf ├── appSettings.heroku.json ├── src ├── TodoBackend.Api │ ├── runtimeconfig.template.json │ ├── Data │ │ ├── InMemoryUnitOfWorkManager.cs │ │ ├── EfUnitOfWorkManager.cs │ │ ├── NoOpTransaction.cs │ │ ├── TodoContext.cs │ │ ├── EfTransaction.cs │ │ ├── EfUnitOfWork.cs │ │ └── InMemoryUnitOfWork.cs │ ├── Views │ │ └── TodoView.cs │ ├── appSettings.json │ ├── Properties │ │ └── launchSettings.json │ ├── web.config │ ├── Program.cs │ ├── Controllers │ │ ├── HelloController.cs │ │ └── TodoController.cs │ ├── TodoBackend.Api.csproj │ ├── Infrastructure │ │ └── SimpleInjectorHandlerConfig.cs │ └── Startup.cs └── TodoBackend.Core │ ├── Domain │ ├── IEntity.cs │ ├── IUnitOfWorkManager.cs │ ├── ITransaction.cs │ ├── IUnitOfWork.cs │ └── Todo.cs │ ├── Ports │ ├── Queries │ │ ├── Messages │ │ │ ├── GetAllTodos.cs │ │ │ └── GetTodo.cs │ │ └── Handlers │ │ │ ├── GetTodoHandler.cs │ │ │ └── GetAllTodosHandler.cs │ └── Commands │ │ ├── Messages │ │ ├── DeleteAllTodos.cs │ │ ├── DeleteTodo.cs │ │ ├── CreateTodo.cs │ │ └── UpdateTodo.cs │ │ └── Handlers │ │ ├── CreateTodoHandler.cs │ │ ├── DeleteTodoHandler.cs │ │ ├── UpdateTodoHandler.cs │ │ └── DeleteAllTodosHandler.cs │ ├── Properties │ └── AssemblyInfo.cs │ └── TodoBackend.Core.csproj ├── appSettings.azure.json ├── appSettings.docker.json ├── Dockerfile ├── appSettings.json ├── .vscode ├── tasks.json └── launch.json ├── NuGet.config ├── docker-compose.yml ├── .gitattributes ├── TodoBackend.sln ├── .gitignore └── README.md /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM yanana/nginx-consul-template 2 | 3 | ADD app.conf /etc/consul-templates/app.conf.ctmpl 4 | -------------------------------------------------------------------------------- /appSettings.heroku.json: -------------------------------------------------------------------------------- 1 | { 2 | "Uris": { 3 | "Api": "https://todo-backend-aspnetcore.herokuapp.com" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/TodoBackend.Api/runtimeconfig.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "configProperties": { 3 | "System.GC.Server": true 4 | } 5 | } -------------------------------------------------------------------------------- /appSettings.azure.json: -------------------------------------------------------------------------------- 1 | { 2 | "Uris": { 3 | "Api": "https://todo-backend-aspnetcore.azurewebsites.net" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/TodoBackend.Core/Domain/IEntity.cs: -------------------------------------------------------------------------------- 1 | namespace TodoBackend.Core.Domain 2 | { 3 | public interface IEntity 4 | { 5 | int Id { get; } 6 | } 7 | } -------------------------------------------------------------------------------- /src/TodoBackend.Core/Domain/IUnitOfWorkManager.cs: -------------------------------------------------------------------------------- 1 | namespace TodoBackend.Core.Domain 2 | { 3 | public interface IUnitOfWorkManager 4 | { 5 | IUnitOfWork Start(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /nginx/app.conf: -------------------------------------------------------------------------------- 1 | upstream app { 2 | least_conn; 3 | {{range service "api"}}server {{.Address}}:{{.Port}} max_fails=3 fail_timeout=60 weight=1; 4 | {{else}}server 127.0.0.1:65535; # force a 502{{end}} 5 | } -------------------------------------------------------------------------------- /appSettings.docker.json: -------------------------------------------------------------------------------- 1 | { 2 | "Uris": { 3 | "Api": "http://localhost:8080" 4 | }, 5 | "DataStore": "SqlServer", 6 | "ConnectionStrings": { 7 | "SqlServer": "Server=sqlserver;Database=TodoBackend;User Id=sa;Password=P@ssword1;" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM microsoft/dotnet:latest 2 | 3 | COPY . /app 4 | WORKDIR /app 5 | 6 | RUN dotnet restore 7 | RUN dotnet publish src/TodoBackend.Api/TodoBackend.Api.csproj -c Release -o ../../bin 8 | 9 | EXPOSE 5000 10 | 11 | ENTRYPOINT [ "dotnet", "bin/TodoBackend.Api.dll" ] -------------------------------------------------------------------------------- /src/TodoBackend.Core/Ports/Queries/Messages/GetAllTodos.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Darker; 3 | using TodoBackend.Core.Domain; 4 | 5 | namespace TodoBackend.Core.Ports.Queries.Messages 6 | { 7 | public sealed class GetAllTodos : IQuery> 8 | { 9 | } 10 | } -------------------------------------------------------------------------------- /appSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | }, 10 | "Uris": { 11 | "Api": "http://localhost:5000" 12 | }, 13 | "DataStore": "InMemory" 14 | } 15 | -------------------------------------------------------------------------------- /src/TodoBackend.Api/Data/InMemoryUnitOfWorkManager.cs: -------------------------------------------------------------------------------- 1 | using TodoBackend.Core.Domain; 2 | 3 | namespace TodoBackend.Api.Data 4 | { 5 | internal sealed class InMemoryUnitOfWorkManager : IUnitOfWorkManager 6 | { 7 | public IUnitOfWork Start() 8 | { 9 | return new InMemoryUnitOfWork(); 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/TodoBackend.Core/Ports/Commands/Messages/DeleteAllTodos.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Paramore.Brighter; 3 | 4 | namespace TodoBackend.Core.Ports.Commands.Messages 5 | { 6 | public sealed class DeleteAllTodos : Command 7 | { 8 | public DeleteAllTodos() 9 | : base(Guid.NewGuid()) 10 | { 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/TodoBackend.Api/Views/TodoView.cs: -------------------------------------------------------------------------------- 1 | namespace TodoBackend.Api.Views 2 | { 3 | public sealed class TodoView 4 | { 5 | public int Id { get; set; } 6 | public int? Order { get; set; } 7 | public string Title { get; set; } 8 | public string Url { get; set; } 9 | public bool Completed { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /src/TodoBackend.Api/appSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | }, 10 | "Uris": { 11 | "Api": "https://todo-backend-aspnetcore.azurewebsites.net" 12 | }, 13 | "DataStore": "InMemory" 14 | } 15 | -------------------------------------------------------------------------------- /src/TodoBackend.Core/Domain/ITransaction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace TodoBackend.Core.Domain 6 | { 7 | public interface ITransaction : IDisposable 8 | { 9 | Task CommitAsync(CancellationToken cancellationToken = default(CancellationToken)); 10 | void Rollback(); 11 | } 12 | } -------------------------------------------------------------------------------- /src/TodoBackend.Core/Ports/Queries/Messages/GetTodo.cs: -------------------------------------------------------------------------------- 1 | using Darker; 2 | using TodoBackend.Core.Domain; 3 | 4 | namespace TodoBackend.Core.Ports.Queries.Messages 5 | { 6 | public sealed class GetTodo : IQuery 7 | { 8 | public int TodoId { get; } 9 | 10 | public GetTodo(int todoId) 11 | { 12 | TodoId = todoId; 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/TodoBackend.Core/Ports/Commands/Messages/DeleteTodo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Paramore.Brighter; 3 | 4 | namespace TodoBackend.Core.Ports.Commands.Messages 5 | { 6 | public sealed class DeleteTodo : Command 7 | { 8 | public int TodoId { get; } 9 | 10 | public DeleteTodo(int todoId) 11 | : base(Guid.NewGuid()) 12 | { 13 | TodoId = todoId; 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/TodoBackend.Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:52552/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "TodoBackend.Api": { 12 | "commandName": "Project", 13 | "applicationUrl": "http://localhost:5000" 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "command": "dotnet", 4 | "isShellCommand": true, 5 | "args": [], 6 | "tasks": [ 7 | { 8 | "taskName": "build", 9 | "args": [ 10 | "${workspaceRoot}\\src\\TodoBackend.Api\\TodoBackend.Api.csproj" 11 | ], 12 | "isBuildCommand": true, 13 | "problemMatcher": "$msCompile" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/TodoBackend.Api/Data/EfUnitOfWorkManager.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using TodoBackend.Core.Domain; 3 | 4 | namespace TodoBackend.Api.Data 5 | { 6 | internal sealed class EfUnitOfWorkManager : IUnitOfWorkManager 7 | { 8 | private readonly DbContextOptions _options; 9 | 10 | public EfUnitOfWorkManager(DbContextOptions options) 11 | { 12 | _options = options; 13 | } 14 | 15 | public IUnitOfWork Start() 16 | { 17 | return new EfUnitOfWork(_options); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/TodoBackend.Api/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/TodoBackend.Api/Data/NoOpTransaction.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using TodoBackend.Core.Domain; 4 | 5 | namespace TodoBackend.Api.Data 6 | { 7 | internal sealed class NoOpTransaction : ITransaction 8 | { 9 | public Task CommitAsync(CancellationToken cancellationToken = new CancellationToken()) 10 | { 11 | // nothing to do 12 | return Task.FromResult(0); 13 | } 14 | 15 | public void Rollback() 16 | { 17 | // nothing to do 18 | } 19 | public void Dispose() 20 | { 21 | // nothing to do 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/TodoBackend.Api/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Microsoft.AspNetCore.Hosting; 4 | 5 | namespace TodoBackend.Api 6 | { 7 | public class Program 8 | { 9 | public static void Main(string[] args) 10 | { 11 | Console.Title = "TodoBackend.Api"; 12 | 13 | var host = new WebHostBuilder() 14 | .UseKestrel() 15 | .UseUrls("http://+:5000") 16 | .UseContentRoot(Directory.GetCurrentDirectory()) 17 | .UseIISIntegration() 18 | .UseStartup() 19 | .Build(); 20 | 21 | host.Run(); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/TodoBackend.Core/Ports/Commands/Messages/CreateTodo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Paramore.Brighter; 3 | 4 | namespace TodoBackend.Core.Ports.Commands.Messages 5 | { 6 | public sealed class CreateTodo : Command 7 | { 8 | public int TodoId { get; } 9 | public string Title { get; } 10 | public bool Completed { get; } 11 | public int? Order { get; } 12 | 13 | public CreateTodo(int todoId, string title, bool completed, int? order) 14 | : base(Guid.NewGuid()) 15 | { 16 | TodoId = todoId; 17 | Title = title; 18 | Completed = completed; 19 | Order = order; 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/TodoBackend.Core/Ports/Commands/Messages/UpdateTodo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Paramore.Brighter; 3 | 4 | namespace TodoBackend.Core.Ports.Commands.Messages 5 | { 6 | public sealed class UpdateTodo : Command 7 | { 8 | public int TodoId { get; } 9 | public string Title { get; } 10 | public bool Completed { get; } 11 | public int? Order { get; } 12 | 13 | public UpdateTodo(int todoId, string title, bool completed, int? order) 14 | : base(Guid.NewGuid()) 15 | { 16 | TodoId = todoId; 17 | Title = title ?? string.Empty; 18 | Completed = completed; 19 | Order = order; 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/TodoBackend.Core/Domain/IUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace TodoBackend.Core.Domain 8 | { 9 | public interface IUnitOfWork : IDisposable 10 | { 11 | Task GetAsync(int id, CancellationToken cancellationToken = default(CancellationToken)) where T : class, IEntity; 12 | Task> GetAllAsync(CancellationToken cancellationToken = default(CancellationToken)) where T : class, IEntity; 13 | void Add(T entity) where T : class, IEntity; 14 | void Delete(T entity) where T : class, IEntity; 15 | 16 | Task BeginTransactionAsync(IsolationLevel isolationLevel = IsolationLevel.ReadCommitted, CancellationToken cancellationToken = default(CancellationToken)); 17 | } 18 | } -------------------------------------------------------------------------------- /src/TodoBackend.Api/Data/TodoContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using TodoBackend.Core.Domain; 3 | 4 | namespace TodoBackend.Api.Data 5 | { 6 | public class TodoContext : DbContext 7 | { 8 | public DbSet Todos { get; set; } 9 | 10 | public TodoContext(DbContextOptions options) 11 | : base(options) 12 | { } 13 | 14 | protected override void OnModelCreating(ModelBuilder builder) 15 | { 16 | base.OnModelCreating(builder); 17 | 18 | builder.Entity(entity => 19 | { 20 | entity.HasKey("SequenceId"); 21 | entity.HasAlternateKey(e => e.Id); 22 | 23 | entity.Property(e => e.Title).IsRequired().HasMaxLength(255); 24 | entity.Property(e => e.Completed).IsRequired(); 25 | }); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/TodoBackend.Core/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyConfiguration("")] 9 | [assembly: AssemblyCompany("")] 10 | [assembly: AssemblyProduct("TodoBackend.Core")] 11 | [assembly: AssemblyTrademark("")] 12 | 13 | // Setting ComVisible to false makes the types in this assembly not visible 14 | // to COM components. If you need to access a type in this assembly from 15 | // COM, set the ComVisible attribute to true on that type. 16 | [assembly: ComVisible(false)] 17 | 18 | // The following GUID is for the ID of the typelib if this project is exposed to COM 19 | [assembly: Guid("4d0c8e50-e63b-4ed5-899c-33515a53be8f")] 20 | -------------------------------------------------------------------------------- /src/TodoBackend.Api/Controllers/HelloController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Serilog; 4 | 5 | namespace TodoBackend.Api.Controllers 6 | { 7 | [Route("hello")] 8 | public class HelloController : ControllerBase 9 | { 10 | private readonly ILogger _logger; 11 | 12 | public HelloController(ILogger logger) 13 | { 14 | _logger = logger; 15 | } 16 | 17 | [HttpGet] 18 | public IActionResult Get() 19 | { 20 | _logger.Information("Hello there, this is an example log with a random number: {number}", new Random().Next()); 21 | 22 | var hostName = Environment.GetEnvironmentVariable("DYNO") 23 | ?? Environment.GetEnvironmentVariable("COMPUTERNAME") 24 | ?? Environment.GetEnvironmentVariable("HOSTNAME"); 25 | 26 | return Ok($"Hello from {hostName}"); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/TodoBackend.Core/Domain/Todo.cs: -------------------------------------------------------------------------------- 1 | namespace TodoBackend.Core.Domain 2 | { 3 | public class Todo : IEntity 4 | { 5 | // todo 6 | private int SequenceId { get; set; } 7 | 8 | public int Id { get; private set; } 9 | public string Title { get; private set; } 10 | public bool Completed { get; private set; } 11 | public int? Order { get; private set; } 12 | 13 | // todo 14 | private Todo() 15 | { 16 | } 17 | 18 | public Todo(int id, string title, bool completed, int? order) 19 | { 20 | Id = id; 21 | Title = title; 22 | Completed = completed; 23 | Order = order; 24 | } 25 | 26 | public void Update(string title, bool completed, int? order) 27 | { 28 | Title = title; 29 | Completed = completed; 30 | Order = order; 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/TodoBackend.Core/Ports/Queries/Handlers/GetTodoHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Darker; 4 | using Darker.RequestLogging; 5 | using TodoBackend.Core.Domain; 6 | using TodoBackend.Core.Ports.Queries.Messages; 7 | 8 | namespace TodoBackend.Core.Ports.Queries.Handlers 9 | { 10 | public sealed class GetTodoHandler : AsyncQueryHandler 11 | { 12 | private readonly IUnitOfWorkManager _unitOfWorkManager; 13 | 14 | public GetTodoHandler(IUnitOfWorkManager unitOfWorkManager) 15 | { 16 | _unitOfWorkManager = unitOfWorkManager; 17 | } 18 | 19 | [RequestLogging(1)] 20 | public override async Task ExecuteAsync(GetTodo request, CancellationToken cancellationToken = default(CancellationToken)) 21 | { 22 | using (var uow = _unitOfWorkManager.Start()) 23 | { 24 | return await uow.GetAsync(request.TodoId, cancellationToken).ConfigureAwait(false); 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | sqlserver: 5 | image: microsoft/mssql-server-linux 6 | ports: 7 | - 1433 8 | volumes: 9 | - /opt/mssql/data:/opt/mssql/data 10 | environment: 11 | - ACCEPT_EULA=Y 12 | - SA_PASSWORD=P@ssword1 13 | 14 | api: 15 | image: todobackend-api 16 | build: 17 | context: . 18 | environment: 19 | - SERVICE_NAME=api 20 | - ASPNETCORE_ENVIRONMENT=docker 21 | ports: 22 | - 5000 23 | links: 24 | - sqlserver 25 | 26 | loadbalancer: 27 | image: nginx 28 | build: 29 | context: ./nginx 30 | depends_on: 31 | - api 32 | ports: 33 | - "8080:80" 34 | 35 | consul: 36 | image: consul 37 | ports: 38 | - "8300:8300" 39 | - "8400:8400" 40 | - "8500:8500" 41 | - "8600:53/udp" 42 | 43 | registrator: 44 | image: gliderlabs/registrator 45 | depends_on: 46 | - consul 47 | command: -internal -resync 600 consul://consul:8500 48 | volumes: 49 | - "/var/run/docker.sock:/tmp/docker.sock" 50 | -------------------------------------------------------------------------------- /src/TodoBackend.Api/Data/EfTransaction.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Microsoft.EntityFrameworkCore.Storage; 4 | using TodoBackend.Core.Domain; 5 | 6 | namespace TodoBackend.Api.Data 7 | { 8 | internal sealed class EfTransaction : ITransaction 9 | { 10 | private readonly TodoContext _context; 11 | private readonly IDbContextTransaction _transaction; 12 | 13 | public EfTransaction(TodoContext context, IDbContextTransaction transaction) 14 | { 15 | _context = context; 16 | _transaction = transaction; 17 | } 18 | 19 | public async Task CommitAsync(CancellationToken cancellationToken = default(CancellationToken)) 20 | { 21 | await _context.SaveChangesAsync(cancellationToken); 22 | 23 | _transaction.Commit(); 24 | } 25 | 26 | public void Rollback() 27 | { 28 | _transaction.Rollback(); 29 | } 30 | 31 | public void Dispose() 32 | { 33 | _transaction.Dispose(); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/TodoBackend.Core/Ports/Queries/Handlers/GetAllTodosHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Darker; 5 | using Darker.RequestLogging; 6 | using TodoBackend.Core.Domain; 7 | using TodoBackend.Core.Ports.Queries.Messages; 8 | 9 | namespace TodoBackend.Core.Ports.Queries.Handlers 10 | { 11 | public sealed class GetAllTodosHandler : AsyncQueryHandler> 12 | { 13 | private readonly IUnitOfWorkManager _unitOfWorkManager; 14 | 15 | public GetAllTodosHandler(IUnitOfWorkManager unitOfWorkManager) 16 | { 17 | _unitOfWorkManager = unitOfWorkManager; 18 | } 19 | 20 | [RequestLogging(1)] 21 | public override async Task> ExecuteAsync(GetAllTodos request, CancellationToken cancellationToken = default(CancellationToken)) 22 | { 23 | using (var uow = _unitOfWorkManager.Start()) 24 | { 25 | return await uow.GetAllAsync(cancellationToken).ConfigureAwait(false); 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/TodoBackend.Core/TodoBackend.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard1.6 4 | TodoBackend.Core 5 | $(PackageTargetFallback);dnxcore50 6 | false 7 | false 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | $(DefineConstants);RELEASE 20 | 21 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": ".NET Core Launch (web)", 6 | "type": "coreclr", 7 | "request": "launch", 8 | "preLaunchTask": "build", 9 | "program": "${workspaceRoot}\\src\\TodoBackend.Api\\bin\\Debug\\netcoreapp1.1\\TodoBackend.Api.dll", 10 | "args": [], 11 | "cwd": "${workspaceRoot}", 12 | "stopAtEntry": false, 13 | "internalConsoleOptions": "openOnSessionStart", 14 | "launchBrowser": { 15 | "enabled": true, 16 | "args": "${auto-detect-url}", 17 | "windows": { 18 | "command": "cmd.exe", 19 | "args": "/C start ${auto-detect-url}" 20 | }, 21 | "osx": { 22 | "command": "open" 23 | }, 24 | "linux": { 25 | "command": "xdg-open" 26 | } 27 | }, 28 | "env": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | }, 31 | "sourceFileMap": { 32 | "/Views": "${workspaceRoot}/Views" 33 | } 34 | }, 35 | { 36 | "name": ".NET Core Attach", 37 | "type": "coreclr", 38 | "request": "attach", 39 | "processId": "${command.pickProcess}" 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /src/TodoBackend.Core/Ports/Commands/Handlers/CreateTodoHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Paramore.Brighter; 4 | using Paramore.Brighter.Logging.Attributes; 5 | using TodoBackend.Core.Domain; 6 | using TodoBackend.Core.Ports.Commands.Messages; 7 | 8 | namespace TodoBackend.Core.Ports.Commands.Handlers 9 | { 10 | public sealed class CreateTodoHandler : RequestHandlerAsync 11 | { 12 | private readonly IUnitOfWorkManager _unitOfWorkManager; 13 | 14 | public CreateTodoHandler(IUnitOfWorkManager unitOfWorkManager) 15 | { 16 | _unitOfWorkManager = unitOfWorkManager; 17 | } 18 | 19 | [RequestLoggingAsync(1, HandlerTiming.Before)] 20 | public override async Task HandleAsync(CreateTodo command, CancellationToken cancellationToken = default(CancellationToken)) 21 | { 22 | using (var uow = _unitOfWorkManager.Start()) 23 | using (var tx = await uow.BeginTransactionAsync(cancellationToken: cancellationToken).ConfigureAwait(ContinueOnCapturedContext)) 24 | { 25 | var todo = new Todo(command.TodoId, command.Title, command.Completed, command.Order); 26 | uow.Add(todo); 27 | 28 | await tx.CommitAsync(cancellationToken).ConfigureAwait(ContinueOnCapturedContext); 29 | } 30 | 31 | return await base.HandleAsync(command, cancellationToken).ConfigureAwait(ContinueOnCapturedContext); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/TodoBackend.Core/Ports/Commands/Handlers/DeleteTodoHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Paramore.Brighter; 4 | using Paramore.Brighter.Logging.Attributes; 5 | using TodoBackend.Core.Domain; 6 | using TodoBackend.Core.Ports.Commands.Messages; 7 | 8 | namespace TodoBackend.Core.Ports.Commands.Handlers 9 | { 10 | public sealed class DeleteTodoHandler : RequestHandlerAsync 11 | { 12 | private readonly IUnitOfWorkManager _unitOfWorkManager; 13 | 14 | public DeleteTodoHandler(IUnitOfWorkManager unitOfWorkManager) 15 | { 16 | _unitOfWorkManager = unitOfWorkManager; 17 | } 18 | 19 | [RequestLoggingAsync(1, HandlerTiming.Before)] 20 | public override async Task HandleAsync(DeleteTodo command, CancellationToken cancellationToken = default(CancellationToken)) 21 | { 22 | using (var uow = _unitOfWorkManager.Start()) 23 | using (var tx = await uow.BeginTransactionAsync(cancellationToken: cancellationToken).ConfigureAwait(ContinueOnCapturedContext)) 24 | { 25 | var todo = await uow.GetAsync(command.TodoId, cancellationToken).ConfigureAwait(ContinueOnCapturedContext); 26 | uow.Delete(todo); 27 | 28 | await tx.CommitAsync(cancellationToken).ConfigureAwait(ContinueOnCapturedContext); 29 | } 30 | 31 | return await base.HandleAsync(command, cancellationToken).ConfigureAwait(ContinueOnCapturedContext); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/TodoBackend.Core/Ports/Commands/Handlers/UpdateTodoHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Paramore.Brighter; 4 | using Paramore.Brighter.Logging.Attributes; 5 | using TodoBackend.Core.Domain; 6 | using TodoBackend.Core.Ports.Commands.Messages; 7 | 8 | namespace TodoBackend.Core.Ports.Commands.Handlers 9 | { 10 | public sealed class UpdateTodoHandler : RequestHandlerAsync 11 | { 12 | private readonly IUnitOfWorkManager _unitOfWorkManager; 13 | 14 | public UpdateTodoHandler(IUnitOfWorkManager unitOfWorkManager) 15 | { 16 | _unitOfWorkManager = unitOfWorkManager; 17 | } 18 | 19 | [RequestLoggingAsync(1, HandlerTiming.Before)] 20 | public override async Task HandleAsync(UpdateTodo command, CancellationToken cancellationToken = default(CancellationToken)) 21 | { 22 | using (var uow = _unitOfWorkManager.Start()) 23 | using (var tx = await uow.BeginTransactionAsync(cancellationToken: cancellationToken).ConfigureAwait(ContinueOnCapturedContext)) 24 | { 25 | var todo = await uow.GetAsync(command.TodoId, cancellationToken).ConfigureAwait(ContinueOnCapturedContext); 26 | todo.Update(command.Title, command.Completed, command.Order); 27 | 28 | await tx.CommitAsync(cancellationToken).ConfigureAwait(ContinueOnCapturedContext); 29 | } 30 | 31 | return await base.HandleAsync(command, cancellationToken).ConfigureAwait(ContinueOnCapturedContext); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/TodoBackend.Core/Ports/Commands/Handlers/DeleteAllTodosHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Paramore.Brighter; 4 | using Paramore.Brighter.Logging.Attributes; 5 | using TodoBackend.Core.Domain; 6 | using TodoBackend.Core.Ports.Commands.Messages; 7 | 8 | namespace TodoBackend.Core.Ports.Commands.Handlers 9 | { 10 | public sealed class DeleteAllTodosHandler : RequestHandlerAsync 11 | { 12 | private readonly IUnitOfWorkManager _unitOfWorkManager; 13 | 14 | public DeleteAllTodosHandler(IUnitOfWorkManager unitOfWorkManager) 15 | { 16 | _unitOfWorkManager = unitOfWorkManager; 17 | } 18 | 19 | [RequestLoggingAsync(1, HandlerTiming.Before)] 20 | public override async Task HandleAsync(DeleteAllTodos command, CancellationToken cancellationToken = default(CancellationToken)) 21 | { 22 | using (var uow = _unitOfWorkManager.Start()) 23 | using (var tx = await uow.BeginTransactionAsync(cancellationToken: cancellationToken).ConfigureAwait(ContinueOnCapturedContext)) 24 | { 25 | var todos = await uow.GetAllAsync(cancellationToken).ConfigureAwait(ContinueOnCapturedContext); 26 | 27 | foreach (var todo in todos) 28 | { 29 | uow.Delete(todo); 30 | } 31 | 32 | await tx.CommitAsync(cancellationToken).ConfigureAwait(ContinueOnCapturedContext); 33 | } 34 | 35 | return await base.HandleAsync(command, cancellationToken).ConfigureAwait(ContinueOnCapturedContext); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Code 5 | *.cs diff=csharp 6 | 7 | # Visual Studio 8 | *.sln text eol=crlf merge=union 9 | *.csproj merge=union 10 | *.vbproj merge=union 11 | *.fsproj merge=union 12 | *.dbproj merge=union 13 | 14 | # Documents 15 | *.doc diff=astextplain 16 | *.DOC diff=astextplain 17 | *.docx diff=astextplain 18 | *.DOCX diff=astextplain 19 | *.dot diff=astextplain 20 | *.DOT diff=astextplain 21 | *.pdf diff=astextplain 22 | *.PDF diff=astextplain 23 | *.rtf diff=astextplain 24 | *.RTF diff=astextplain 25 | *.adoc text 26 | *.textile text 27 | *.mustache text 28 | *.csv text 29 | *.tab text 30 | *.tsv text 31 | *.sql text 32 | 33 | # Graphics 34 | *.png binary 35 | *.jpg binary 36 | *.jpeg binary 37 | *.gif binary 38 | *.ico binary 39 | *.svg text 40 | 41 | # source code 42 | *.php text 43 | *.css text 44 | *.sass text 45 | *.scss text 46 | *.less text 47 | *.styl text 48 | *.js text 49 | *.coffee text 50 | *.json text 51 | *.htm text 52 | *.html text 53 | *.xml text 54 | *.txt text 55 | *.ini text 56 | *.inc text 57 | *.pl text 58 | *.rb text 59 | *.py text 60 | *.scm text 61 | *.sql text 62 | *.sh text 63 | *.bat text 64 | 65 | # templates 66 | *.hbt text 67 | *.jade text 68 | *.haml text 69 | *.hbs text 70 | *.dot text 71 | *.tmpl text 72 | *.phtml text 73 | 74 | # server config 75 | .htaccess text 76 | 77 | # git config 78 | .gitattributes text 79 | .gitignore text 80 | 81 | # code analysis config 82 | .jshintrc text 83 | .jscsrc text 84 | .jshintignore text 85 | .csslintrc text 86 | 87 | # misc config 88 | *.yaml text 89 | *.yml text 90 | .editorconfig text 91 | 92 | # build config 93 | *.npmignore text 94 | *.bowerrc text 95 | 96 | # Heroku 97 | Procfile text 98 | .slugignore text 99 | 100 | # Documentation 101 | *.md text 102 | LICENSE text 103 | AUTHORS text 104 | -------------------------------------------------------------------------------- /src/TodoBackend.Api/Data/EfUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Data; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Microsoft.EntityFrameworkCore; 7 | using TodoBackend.Core.Domain; 8 | 9 | namespace TodoBackend.Api.Data 10 | { 11 | internal sealed class EfUnitOfWork : IUnitOfWork 12 | { 13 | private readonly TodoContext _context; 14 | 15 | public EfUnitOfWork(DbContextOptions options) 16 | { 17 | _context = new TodoContext(options); 18 | } 19 | 20 | public async Task GetAsync(int id, CancellationToken cancellationToken = default(CancellationToken)) 21 | where T : class, IEntity 22 | { 23 | return await _context.Set().SingleOrDefaultAsync(e => e.Id == id, cancellationToken).ConfigureAwait(false); 24 | } 25 | 26 | public async Task> GetAllAsync(CancellationToken cancellationToken = new CancellationToken()) where T : class, IEntity 27 | { 28 | return await _context.Set().ToListAsync(cancellationToken).ConfigureAwait(false); 29 | } 30 | 31 | public void Add(T entity) where T : class, IEntity 32 | { 33 | _context.Set().Add(entity); 34 | } 35 | 36 | public void Delete(T entity) where T : class, IEntity 37 | { 38 | _context.Set().Remove(entity); 39 | } 40 | 41 | public IQueryable AsQueryable() where T : class, IEntity 42 | { 43 | return _context.Set(); 44 | } 45 | 46 | public async Task BeginTransactionAsync(IsolationLevel isolationLevel = IsolationLevel.ReadCommitted, CancellationToken cancellationToken = default(CancellationToken)) 47 | { 48 | var tx = await _context.Database.BeginTransactionAsync(isolationLevel, cancellationToken).ConfigureAwait(false); 49 | return new EfTransaction(_context, tx); 50 | } 51 | 52 | public void Dispose() 53 | { 54 | _context.Dispose(); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/TodoBackend.Api/Data/InMemoryUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Data; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using TodoBackend.Core.Domain; 9 | 10 | namespace TodoBackend.Api.Data 11 | { 12 | internal sealed class InMemoryUnitOfWork : IUnitOfWork 13 | { 14 | private static readonly ConcurrentDictionary _todos = new ConcurrentDictionary(); 15 | 16 | public Task GetAsync(int id, CancellationToken cancellationToken = default(CancellationToken)) 17 | where T : class, IEntity 18 | { 19 | if (typeof(T) != typeof(Todo)) 20 | throw new NotSupportedException(); 21 | 22 | if (_todos.TryGetValue(id, out Todo todo)) 23 | { 24 | return Task.FromResult((T)Convert.ChangeType(todo, typeof(T))); 25 | } 26 | 27 | return Task.FromResult(default(T)); 28 | } 29 | 30 | public Task> GetAllAsync(CancellationToken cancellationToken = new CancellationToken()) where T : class, IEntity 31 | { 32 | if (typeof(T) != typeof(Todo)) 33 | throw new NotSupportedException(); 34 | 35 | return Task.FromResult(_todos.Values.Cast()); 36 | } 37 | 38 | public void Add(T entity) where T : class, IEntity 39 | { 40 | var todo = entity as Todo; 41 | if (todo == null) 42 | throw new NotSupportedException(); 43 | 44 | _todos.AddOrUpdate(entity.Id, todo, (id, existing) => todo); 45 | } 46 | 47 | public void Delete(T entity) where T : class, IEntity 48 | { 49 | if (typeof(T) != typeof(Todo)) 50 | throw new NotSupportedException(); 51 | 52 | _todos.TryRemove(entity.Id, out Todo existing); 53 | 54 | // just ignore errors 55 | } 56 | 57 | public Task BeginTransactionAsync(IsolationLevel isolationLevel = IsolationLevel.ReadCommitted, CancellationToken cancellationToken = default(CancellationToken)) 58 | { 59 | return Task.FromResult(new NoOpTransaction()); 60 | } 61 | 62 | public void Dispose() 63 | { 64 | // nothing to do 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /src/TodoBackend.Api/TodoBackend.Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | netcoreapp1.1 5 | true 6 | TodoBackend.Api 7 | $(PackageTargetFallback);dotnet5.6;portable-net45+win8 8 | 9 | 10 | 11 | 12 | 13 | 14 | 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 | 43 | 44 | 45 | 46 | 47 | 48 | 1.0.0-msbuild2-final 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/TodoBackend.Api/Infrastructure/SimpleInjectorHandlerConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using Paramore.Brighter; 6 | using Paramore.Brighter.Logging.Handlers; 7 | using Paramore.Brighter.Policies.Handlers; 8 | using SimpleInjector; 9 | 10 | namespace TodoBackend.Api.Infrastructure 11 | { 12 | public class SimpleInjectorHandlerConfig 13 | { 14 | private readonly Container _container; 15 | private readonly HandlerFactory _handlerFactory; 16 | 17 | public IAmASubscriberRegistry Subscribers => _handlerFactory; 18 | public HandlerConfiguration HandlerConfiguration => new HandlerConfiguration(_handlerFactory, _handlerFactory); 19 | 20 | public SimpleInjectorHandlerConfig(Container container) 21 | { 22 | _container = container; 23 | _handlerFactory = new HandlerFactory(container); 24 | } 25 | 26 | public void RegisterDefaultHandlers() 27 | { 28 | _container.Register(typeof(RequestLoggingHandler<>)); 29 | _container.Register(typeof(ExceptionPolicyHandler<>)); 30 | _container.Register(typeof(RequestLoggingHandlerAsync<>)); 31 | _container.Register(typeof(ExceptionPolicyHandlerAsync<>)); 32 | } 33 | 34 | public void RegisterSubscribersFromAssembly(Assembly assembly) 35 | { 36 | var subscribers = 37 | from t in assembly.GetExportedTypes() 38 | let ti = t.GetTypeInfo() 39 | where ti.IsClass && !ti.IsAbstract && !ti.IsInterface 40 | from i in t.GetInterfaces() 41 | where i.GetTypeInfo().IsGenericType && (i.GetGenericTypeDefinition() == typeof(IHandleRequestsAsync<>)) 42 | select new { Request = i.GetGenericArguments().First(), Handler = t }; 43 | 44 | foreach (var subscriber in subscribers) 45 | { 46 | _handlerFactory.Register(subscriber.Request, subscriber.Handler); 47 | } 48 | } 49 | 50 | private sealed class HandlerFactory : IAmAHandlerFactoryAsync, IAmASubscriberRegistry 51 | { 52 | private readonly Container _container; 53 | private readonly SubscriberRegistry _registry; 54 | 55 | public HandlerFactory(Container container) 56 | { 57 | _container = container; 58 | _registry = new SubscriberRegistry(); 59 | } 60 | 61 | IHandleRequestsAsync IAmAHandlerFactoryAsync.Create(Type handlerType) 62 | { 63 | return (IHandleRequestsAsync)_container.GetInstance(handlerType); 64 | } 65 | 66 | public void Release(IHandleRequestsAsync handler) 67 | { 68 | // todo not supported by all containers 69 | } 70 | 71 | public IEnumerable Get() where T : class, IRequest 72 | { 73 | return _registry.Get(); 74 | } 75 | 76 | public void Register() where TRequest : class, IRequest where TImplementation : class, IHandleRequests 77 | { 78 | _container.Register(); 79 | _registry.Register(); 80 | } 81 | 82 | public void Register(Type request, Type handler) 83 | { 84 | _container.Register(handler); 85 | _registry.Add(request, handler); 86 | } 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /TodoBackend.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26430.14 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8465AC72-312C-4C4B-9576-B053253ABB3C}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{28676B04-D50C-46A0-ACA2-BE4B79A4ECC2}" 9 | ProjectSection(SolutionItems) = preProject 10 | docker-compose.yml = docker-compose.yml 11 | Dockerfile = Dockerfile 12 | NuGet.config = NuGet.config 13 | README.md = README.md 14 | EndProjectSection 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoBackend.Api", "src\TodoBackend.Api\TodoBackend.Api.csproj", "{DF9CAC82-A0E6-4583-8E3F-4DFC755C0EC5}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoBackend.Core", "src\TodoBackend.Core\TodoBackend.Core.csproj", "{4D0C8E50-E63B-4ED5-899C-33515A53BE8F}" 19 | EndProject 20 | Global 21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 22 | Debug|Any CPU = Debug|Any CPU 23 | Debug|x64 = Debug|x64 24 | Debug|x86 = Debug|x86 25 | Release|Any CPU = Release|Any CPU 26 | Release|x64 = Release|x64 27 | Release|x86 = Release|x86 28 | EndGlobalSection 29 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 30 | {DF9CAC82-A0E6-4583-8E3F-4DFC755C0EC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {DF9CAC82-A0E6-4583-8E3F-4DFC755C0EC5}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {DF9CAC82-A0E6-4583-8E3F-4DFC755C0EC5}.Debug|x64.ActiveCfg = Debug|Any CPU 33 | {DF9CAC82-A0E6-4583-8E3F-4DFC755C0EC5}.Debug|x64.Build.0 = Debug|Any CPU 34 | {DF9CAC82-A0E6-4583-8E3F-4DFC755C0EC5}.Debug|x86.ActiveCfg = Debug|Any CPU 35 | {DF9CAC82-A0E6-4583-8E3F-4DFC755C0EC5}.Debug|x86.Build.0 = Debug|Any CPU 36 | {DF9CAC82-A0E6-4583-8E3F-4DFC755C0EC5}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {DF9CAC82-A0E6-4583-8E3F-4DFC755C0EC5}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {DF9CAC82-A0E6-4583-8E3F-4DFC755C0EC5}.Release|x64.ActiveCfg = Release|Any CPU 39 | {DF9CAC82-A0E6-4583-8E3F-4DFC755C0EC5}.Release|x64.Build.0 = Release|Any CPU 40 | {DF9CAC82-A0E6-4583-8E3F-4DFC755C0EC5}.Release|x86.ActiveCfg = Release|Any CPU 41 | {DF9CAC82-A0E6-4583-8E3F-4DFC755C0EC5}.Release|x86.Build.0 = Release|Any CPU 42 | {4D0C8E50-E63B-4ED5-899C-33515A53BE8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {4D0C8E50-E63B-4ED5-899C-33515A53BE8F}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {4D0C8E50-E63B-4ED5-899C-33515A53BE8F}.Debug|x64.ActiveCfg = Debug|Any CPU 45 | {4D0C8E50-E63B-4ED5-899C-33515A53BE8F}.Debug|x64.Build.0 = Debug|Any CPU 46 | {4D0C8E50-E63B-4ED5-899C-33515A53BE8F}.Debug|x86.ActiveCfg = Debug|Any CPU 47 | {4D0C8E50-E63B-4ED5-899C-33515A53BE8F}.Debug|x86.Build.0 = Debug|Any CPU 48 | {4D0C8E50-E63B-4ED5-899C-33515A53BE8F}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {4D0C8E50-E63B-4ED5-899C-33515A53BE8F}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {4D0C8E50-E63B-4ED5-899C-33515A53BE8F}.Release|x64.ActiveCfg = Release|Any CPU 51 | {4D0C8E50-E63B-4ED5-899C-33515A53BE8F}.Release|x64.Build.0 = Release|Any CPU 52 | {4D0C8E50-E63B-4ED5-899C-33515A53BE8F}.Release|x86.ActiveCfg = Release|Any CPU 53 | {4D0C8E50-E63B-4ED5-899C-33515A53BE8F}.Release|x86.Build.0 = Release|Any CPU 54 | EndGlobalSection 55 | GlobalSection(SolutionProperties) = preSolution 56 | HideSolutionNode = FALSE 57 | EndGlobalSection 58 | GlobalSection(NestedProjects) = preSolution 59 | {DF9CAC82-A0E6-4583-8E3F-4DFC755C0EC5} = {8465AC72-312C-4C4B-9576-B053253ABB3C} 60 | {4D0C8E50-E63B-4ED5-899C-33515A53BE8F} = {8465AC72-312C-4C4B-9576-B053253ABB3C} 61 | EndGlobalSection 62 | EndGlobal 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | 24 | # Generated Config Files 25 | appSettings.json 26 | 27 | # Visual Studo 2015 cache/options directory 28 | .vs/ 29 | 30 | # Visual Studio JavaScript IntelliSense 31 | _references.js 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | *_i.c 47 | *_p.c 48 | *_i.h 49 | *.ilk 50 | *.meta 51 | *.obj 52 | *.pch 53 | *.pdb 54 | *.pgc 55 | *.pgd 56 | *.rsp 57 | *.sbr 58 | *.tlb 59 | *.tli 60 | *.tlh 61 | *.tmp 62 | *.tmp_proj 63 | *.log 64 | *.vspscc 65 | *.vssscc 66 | .builds 67 | *.pidb 68 | *.svclog 69 | *.scc 70 | 71 | # Chutzpah Test files 72 | _Chutzpah* 73 | 74 | # Visual C++ cache files 75 | ipch/ 76 | *.aps 77 | *.ncb 78 | *.opensdf 79 | *.sdf 80 | *.cachefile 81 | 82 | # Visual Studio profiler 83 | *.psess 84 | *.vsp 85 | *.vspx 86 | 87 | # TFS 2012 Local Workspace 88 | $tf/ 89 | 90 | # Guidance Automation Toolkit 91 | *.gpState 92 | 93 | # PyCache 94 | __pycache__/ 95 | 96 | # ReSharper is a .NET coding add-in 97 | _ReSharper*/ 98 | *.[Rr]e[Ss]harper 99 | *.DotSettings.user 100 | 101 | # JustCode is a .NET coding addin-in 102 | .JustCode 103 | 104 | # TeamCity is a build add-in 105 | _TeamCity* 106 | 107 | # DotCover is a Code Coverage Tool 108 | *.dotCover 109 | 110 | # NCrunch 111 | _NCrunch_* 112 | .*crunch*.local.xml 113 | 114 | # MightyMoose 115 | *.mm.* 116 | AutoTest.Net/ 117 | 118 | # Web workbench (sass) 119 | .sass-cache/ 120 | 121 | # Installshield output folder 122 | [Ee]xpress/ 123 | 124 | # DocProject is a documentation generator add-in 125 | DocProject/buildhelp/ 126 | DocProject/Help/*.HxT 127 | DocProject/Help/*.HxC 128 | DocProject/Help/*.hhc 129 | DocProject/Help/*.hhk 130 | DocProject/Help/*.hhp 131 | DocProject/Help/Html2 132 | DocProject/Help/html 133 | 134 | # Click-Once directory 135 | publish/ 136 | PublishProfiles/ 137 | 138 | # Publish Web Output 139 | *.[Pp]ublish.xml 140 | *.azurePubxml 141 | # TODO: Comment the next line if you want to checkin your web deploy settings 142 | # but database connection strings (with potential passwords) will be unencrypted 143 | *.pubxml 144 | *.publishproj 145 | 146 | # NuGet Packages 147 | *.nupkg 148 | # The packages folder can be ignored because of Package Restore 149 | **/packages/* 150 | # except build/, which is used as an MSBuild target. 151 | !**/packages/build/ 152 | # Uncomment if necessary however generally it will be regenerated when needed 153 | #!**/packages/repositories.config 154 | 155 | # Windows Azure Build Output 156 | csx/ 157 | *.build.csdef 158 | 159 | # Windows Store app package directory 160 | AppPackages/ 161 | 162 | # Others 163 | *.[Cc]ache 164 | ClientBin/ 165 | [Ss]tyle[Cc]op.* 166 | ~$* 167 | *~ 168 | *.dbmdl 169 | *.dbproj.schemaview 170 | *.pfx 171 | *.publishsettings 172 | node_modules/ 173 | bower_components/ 174 | project.lock.json 175 | lib/ 176 | 177 | # RIA/Silverlight projects 178 | Generated_Code/ 179 | 180 | # Backup & report files from converting an old project file 181 | # to a newer Visual Studio version. Backup files are not needed, 182 | # because we have git ;-) 183 | _UpgradeReport_Files/ 184 | Backup*/ 185 | UpgradeLog*.XML 186 | UpgradeLog*.htm 187 | 188 | # SQL Server files 189 | *.mdf 190 | *.ldf 191 | 192 | # Business Intelligence projects 193 | *.rdl.data 194 | *.bim.layout 195 | *.bim_*.settings 196 | 197 | # Microsoft Fakes 198 | FakesAssemblies/ 199 | 200 | # Node.js Tools for Visual Studio 201 | .ntvs_analysis.dat 202 | 203 | # Visual Studio 6 build log 204 | *.plg 205 | 206 | # Visual Studio 6 workspace options file 207 | *.opt 208 | -------------------------------------------------------------------------------- /src/TodoBackend.Api/Controllers/TodoController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Darker; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.Extensions.Configuration; 8 | using Paramore.Brighter; 9 | using TodoBackend.Api.Views; 10 | using TodoBackend.Core.Ports.Commands.Messages; 11 | using TodoBackend.Core.Ports.Queries.Messages; 12 | 13 | namespace TodoBackend.Api.Controllers 14 | { 15 | [Route("")] 16 | public class TodoController : ControllerBase 17 | { 18 | private readonly IAmACommandProcessor _commandProcessor; 19 | private readonly IQueryProcessor _queryProcessor; 20 | private readonly IConfiguration _configuration; 21 | 22 | public TodoController(IAmACommandProcessor commandProcessor, IQueryProcessor queryProcessor, IConfiguration configuration) 23 | { 24 | _commandProcessor = commandProcessor; 25 | _queryProcessor = queryProcessor; 26 | _configuration = configuration; 27 | } 28 | 29 | [HttpGet] 30 | public async Task Get(CancellationToken cancellationToken = default(CancellationToken)) 31 | { 32 | var todos = await _queryProcessor.ExecuteAsync(new GetAllTodos(), cancellationToken).ConfigureAwait(false); 33 | 34 | var views = todos.Select(t => new TodoView 35 | { 36 | Id = t.Id, 37 | Title = t.Title, 38 | Completed = t.Completed, 39 | Order = t.Order, 40 | Url = GetTodoUri(t.Id) 41 | }); 42 | 43 | return Ok(views); 44 | } 45 | 46 | [HttpGet("{id}")] 47 | public async Task Get(int id, CancellationToken cancellationToken = default(CancellationToken)) 48 | { 49 | var todo = await _queryProcessor.ExecuteAsync(new GetTodo(id), cancellationToken).ConfigureAwait(false); 50 | if (todo == null) 51 | return NotFound(); 52 | 53 | var view = new TodoView 54 | { 55 | Id = todo.Id, 56 | Title = todo.Title, 57 | Completed = todo.Completed, 58 | Order = todo.Order, 59 | Url = GetTodoUri(todo.Id) 60 | }; 61 | 62 | return Ok(view); 63 | } 64 | 65 | [HttpPost] 66 | public async Task Post([FromBody]TodoView view, CancellationToken cancellationToken = default(CancellationToken)) 67 | { 68 | var id = Math.Abs(Guid.NewGuid().GetHashCode()); 69 | await _commandProcessor.SendAsync(new CreateTodo(id, view.Title, view.Completed, view.Order), false, cancellationToken).ConfigureAwait(false); 70 | 71 | // todo: yeah, this is a hack 72 | view.Id = id; 73 | view.Url = GetTodoUri(id); 74 | 75 | HttpContext.Response.Headers.Add("Location", view.Url); 76 | 77 | return Created(view.Url, view); 78 | } 79 | 80 | [HttpPatch("{id}")] 81 | public async Task Patch(int id, [FromBody]TodoView view, CancellationToken cancellationToken = default(CancellationToken)) 82 | { 83 | await _commandProcessor.SendAsync(new UpdateTodo(id, view.Title, view.Completed, view.Order), false, cancellationToken).ConfigureAwait(false); 84 | 85 | // todo: yeah, this is a hack 86 | view.Id = id; 87 | view.Url = GetTodoUri(id); 88 | 89 | return Ok(view); 90 | } 91 | 92 | [HttpDelete] 93 | public async Task Delete(CancellationToken cancellationToken = default(CancellationToken)) 94 | { 95 | await _commandProcessor.SendAsync(new DeleteAllTodos(), false, cancellationToken).ConfigureAwait(false); 96 | 97 | return Ok(); 98 | } 99 | 100 | [HttpDelete("{id}")] 101 | public async Task Delete(int id, CancellationToken cancellationToken = default(CancellationToken)) 102 | { 103 | await _commandProcessor.SendAsync(new DeleteTodo(id), false, cancellationToken).ConfigureAwait(false); 104 | 105 | return Ok(); 106 | } 107 | 108 | private string GetTodoUri(int id) => $"{_configuration["Uris:Api"]}/{id}"; 109 | } 110 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # todo-backend-aspnetcore 2 | This is an implementation of the [Todo-Backend](http://todobackend.com) API in C# on [ASP.NET Core](https://www.asp.net/core). 3 | 4 | ## Infrastructure 5 | * [Docker](https://www.docker.com) 6 | * [Docker Compose](https://docs.docker.com/compose) for local deployment 7 | * [NGINX](https://nginx.org) for load balancing the api 8 | * [Consul](https://www.consul.io) for service discovery and configuration management 9 | * [ELK Stack](https://www.elastic.co) for log visualisation 10 | 11 | ## Getting started 12 | * Install the latest version of [.NET Core](https://www.microsoft.com/net/core) and [Docker](https://www.docker.com/products/docker) for your platform 13 | * From the project root dir, use [compose](https://docs.docker.com/compose) to build the app, download all infrastructure containers and run the entire stack: 14 | ``` 15 | > docker-compose up -d 16 | ``` 17 | * If everything works, start scaling the API: 18 | ``` 19 | > docker-compose scale api=5 20 | ``` 21 | 22 | ## URIs 23 | * Local Docker (full stack on one VM) 24 | * API: [http://localhost:8080](http://localhost:8080) 25 | * Consul UI: [http://localhost:8500/ui](http://localhost:8500/ui) 26 | * Run ToDo Backend tests [http://todobackend.com/specs/index.html?http://localhost:8080](http://todobackend.com/specs/index.html?http://localhost:8080) 27 | 28 | * Azure (full stack on one VM) 29 | * API: [http://todo-backend-aspnetcore.westeurope.cloudapp.azure.com](http://todo-backend-aspnetcore.westeurope.cloudapp.azure.com/) 30 | * Kibana (ELK): [http://localhost:5601/app/kibana#/discover/Default](http://localhost:5601/app/kibana#/discover/Default]) 31 | * Consul UI: [http://todo-backend-aspnetcore.westeurope.cloudapp.azure.com:8500/ui](http://todo-backend-aspnetcore.westeurope.cloudapp.azure.com:8500/ui) 32 | 33 | * Heroku (only one API instance) 34 | * API: [https://todo-backend-aspnetcore.herokuapp.com](https://todo-backend-aspnetcore.herokuapp.com) 35 | 36 | 37 | # Deployment using Docker 38 | (using Terminal in OSX, starting directory is project root) 39 | 40 | ## Local 41 | * Build and run just the API, printing all stdout from the container: 42 | ``` 43 | $ cd src/TodoBackend.Api/ 44 | $ docker build -t todo-backend-aspnetcore:latest . 45 | $ docker run -p 80:5000 todo-backend-aspnetcore 46 | ``` 47 | 48 | * **OR** run the entire stack in the backgroud and scale the API to 5 containers: 49 | ``` 50 | $ docker-machine up -d 51 | $ docker-machine scale api=5 52 | ``` 53 | 54 | ## Azure 55 | See [https://docs.docker.com/machine/drivers/azure](https://docs.docker.com/machine/drivers/azure) 56 | 57 | * Create resource group with virtual machine 58 | ``` 59 | $ docker-machine create -d azure \ 60 | --azure-ssh-user ops \ 61 | --azure-subscription-id \ 62 | --azure-location westeurope \ 63 | --azure-open-port 80 \ 64 | --azure-open-port 8500 \ 65 | --azure-open-port 5601 \ 66 | machine 67 | ``` 68 | 69 | * Use `docker-machine` to ssh into the VM in Azure, if required 70 | ``` 71 | $ docker-machine ssh machine 72 | ``` 73 | 74 | * Use `docker-machine` to get the VM's public IP address, if required 75 | ``` 76 | $ docker-machine ip machine 77 | ``` 78 | 79 | * We're using version 5 of the ELK stack, which requires at least 2GB of memory. So let's ssh into the VM and once connected, increase the limit (see [Elastic Search documentation](https://www.elastic.co/guide/en/elasticsearch/reference/5.0/vm-max-map-count.html) for details) 80 | ``` 81 | ops@machine:~$ sysctl -w vm.max_map_count=262144 82 | ``` 83 | 84 | * Set the newly created machine as default. This makes all `docker` commands go to the machine in Azure 85 | ``` 86 | $ docker-machine env machine 87 | $ eval $(docker-machine env machine) 88 | ``` 89 | 90 | * Use the regular `docker` or `docker-compose` CLI to start the entire stack or an individual container. Note that this is **NOT** how one would start and scale an infrastructure stack in a real production scenario. 91 | ``` 92 | $ docker-compose up -d 93 | ``` 94 | 95 | ### Optional: Azure CLI 96 | * Install Azure CLI, login, an set mode to Azure Resource Manager (ARM) 97 | ``` 98 | $ npm install -g azure-cli 99 | $ azure login 100 | $ azure config mode arm 101 | ``` 102 | 103 | * If you have multiple subscriptions, use the CLI to show your subscriptions and select a default 104 | ``` 105 | $ azure account list 106 | $ azure account set 107 | ``` 108 | 109 | * Display details of the VM 110 | ``` 111 | $ azure vm show -g docker-machine -n machine 112 | ``` 113 | 114 | * Show public IP and DNS configuration; add a DNS name to the public IP. The VM is now available as http://todo-backend-aspnetcore.westeurope.cloudapp.azure.com 115 | ``` 116 | $ azure network public-ip list -g docker-machine 117 | $ azure network public-ip show -g docker-machine -n machine-ip 118 | $ azure network public-ip create -g docker-machine -n machine-ip -l westeurope -d "todo-backend-aspnetcore" -a "Dynamic" 119 | ``` 120 | 121 | ## Heroku 122 | * Create a Heroku account and create an app (in this example, the app is called `todo-backend-aspnetcore` 123 | * Install the Heroku toolbelt: https://devcenter.heroku.com/articles/heroku-command-line 124 | 125 | ``` 126 | $ cd src/TodoBackend.Api/ 127 | $ docker build -t todo-backend-aspnetcore:latest -f Dockerfile.heroku -e ASPNETCORE_ENVIRONMENT=heroku . 128 | $ docker tag todo-backend-aspnetcore registry.heroku.com/todo-backend-aspnetcore/web 129 | $ docker push registry.heroku.com/todo-backend-aspnetcore/web 130 | $ heroku open --app todo-backend-aspnetcore 131 | ``` 132 | 133 | Alternatively, if the directory contains a Heroku-compatible dockerfile (no `EXPOSE` and listening on `$PORT`) 134 | ``` 135 | $ heroku plugins:install heroku-container-registry 136 | $ heroku heroku container:login 137 | $ heroku container:push web --app todo-backend-aspnetcore 138 | $ heroku open --app todo-backend-aspnetcore 139 | ``` -------------------------------------------------------------------------------- /src/TodoBackend.Api/Startup.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Darker.Builder; 3 | using Darker.RequestLogging; 4 | using Darker.SimpleInjector; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore.Mvc.Controllers; 8 | using Microsoft.AspNetCore.Mvc.ViewComponents; 9 | using Microsoft.EntityFrameworkCore; 10 | using Microsoft.Extensions.Configuration; 11 | using Microsoft.Extensions.DependencyInjection; 12 | using Microsoft.Extensions.Logging; 13 | using Newtonsoft.Json; 14 | using Newtonsoft.Json.Converters; 15 | using Newtonsoft.Json.Serialization; 16 | using Paramore.Brighter; 17 | using Serilog; 18 | using TodoBackend.Api.Infrastructure; 19 | using TodoBackend.Core.Ports.Commands.Handlers; 20 | using TodoBackend.Core.Ports.Queries.Handlers; 21 | using SimpleInjector; 22 | using SimpleInjector.Integration.AspNetCore; 23 | using SimpleInjector.Integration.AspNetCore.Mvc; 24 | using SimpleInjector.Lifestyles; 25 | using TodoBackend.Api.Data; 26 | using TodoBackend.Core.Domain; 27 | 28 | namespace TodoBackend.Api 29 | { 30 | public class Startup 31 | { 32 | private readonly Container _container; 33 | private readonly IConfigurationRoot _configuration; 34 | 35 | public Startup(IHostingEnvironment env) 36 | { 37 | _container = new Container(); 38 | _configuration = new ConfigurationBuilder() 39 | .SetBasePath(env.ContentRootPath) 40 | .AddJsonFile("appSettings.json", optional: false) 41 | .AddJsonFile($"appSettings.{env.EnvironmentName}.json", optional: true) 42 | .AddEnvironmentVariables() 43 | .Build(); 44 | } 45 | 46 | // This method gets called by the runtime. Use this method to add services to the container. 47 | public void ConfigureServices(IServiceCollection services) 48 | { 49 | Log.Logger = new LoggerConfiguration() 50 | .MinimumLevel.Verbose() 51 | .WriteTo.LiterateConsole() 52 | .Enrich.WithMachineName() 53 | .CreateLogger(); 54 | 55 | services.AddCors(); 56 | services.AddMvcCore() 57 | .AddJsonFormatters(opt => 58 | { 59 | opt.ContractResolver = new CamelCasePropertyNamesContractResolver(); 60 | opt.Converters.Add(new StringEnumConverter()); 61 | opt.Formatting = Formatting.Indented; 62 | opt.NullValueHandling = NullValueHandling.Ignore; 63 | }); 64 | 65 | 66 | services.AddSingleton(new SimpleInjectorControllerActivator(_container)); 67 | services.AddSingleton(new SimpleInjectorViewComponentActivator(_container)); 68 | services.UseSimpleInjectorAspNetRequestScoping(_container); 69 | } 70 | 71 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 72 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) 73 | { 74 | loggerFactory.AddSerilog(Log.Logger); 75 | 76 | InitializeContainer(app, loggerFactory); 77 | 78 | _container.Verify(); 79 | 80 | if (env.IsDevelopment()) 81 | { 82 | app.UseDeveloperExceptionPage(); 83 | } 84 | 85 | app.UseCors(opts => opts.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()); 86 | app.UseMvc(); 87 | 88 | EnsureDatabaseCreated(); 89 | } 90 | 91 | // hack 92 | private void EnsureDatabaseCreated() 93 | { 94 | if (_configuration["DataStore"] != "SqlServer") 95 | return; 96 | 97 | var dbopts = new DbContextOptionsBuilder() 98 | .UseSqlServer(_configuration.GetConnectionString("SqlServer")) 99 | .Options; 100 | 101 | using (var ctx = new TodoContext(dbopts)) 102 | { 103 | ctx.Database.EnsureCreated(); 104 | } 105 | } 106 | 107 | private void InitializeContainer(IApplicationBuilder app, ILoggerFactory loggerFactory) 108 | { 109 | _container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle(); 110 | 111 | _container.RegisterMvcControllers(app); 112 | _container.RegisterSingleton(Log.Logger); 113 | 114 | _container.RegisterSingleton(_configuration); 115 | 116 | if (_configuration["DataStore"] == "SqlServer") 117 | { 118 | _container.RegisterSingleton( 119 | new DbContextOptionsBuilder() 120 | .UseSqlServer(_configuration.GetConnectionString("SqlServer")) 121 | .UseLoggerFactory(loggerFactory) 122 | .Options); 123 | 124 | _container.Register(); 125 | } 126 | else 127 | { 128 | _container.Register(); 129 | } 130 | 131 | ConfigureBrighter(); 132 | ConfigureDarker(); 133 | } 134 | 135 | private void ConfigureBrighter() 136 | { 137 | var config = new SimpleInjectorHandlerConfig(_container); 138 | config.RegisterSubscribersFromAssembly(typeof(CreateTodoHandler).GetTypeInfo().Assembly); 139 | config.RegisterDefaultHandlers(); 140 | 141 | var commandProcessor = CommandProcessorBuilder.With() 142 | .Handlers(config.HandlerConfiguration) 143 | .DefaultPolicy() 144 | .NoTaskQueues() 145 | .RequestContextFactory(new InMemoryRequestContextFactory()) 146 | .Build(); 147 | 148 | _container.RegisterSingleton(commandProcessor); 149 | } 150 | 151 | private void ConfigureDarker() 152 | { 153 | var queryProcessor = QueryProcessorBuilder.With() 154 | .SimpleInjectorHandlers(_container, opts => opts 155 | .WithQueriesAndHandlersFromAssembly(typeof(GetTodoHandler).GetTypeInfo().Assembly)) 156 | .InMemoryQueryContextFactory() 157 | .JsonRequestLogging() 158 | .Build(); 159 | 160 | _container.RegisterSingleton(queryProcessor); 161 | } 162 | } 163 | } --------------------------------------------------------------------------------