From 08bca2fea54ab6baa1bdee996a0ce0fa6c00688e Mon Sep 17 00:00:00 2001 From: Thang Chung Date: Fri, 17 Aug 2018 16:15:12 +0700 Subject: [PATCH] Improve code and remove UoW from Handler #3 #4 --- .dockerignore | 9 +++ build.cake | 1 + netcore-kit.sln | 56 ++++++++++++++ samples/TodoApi/.dockerignore | 5 ++ samples/TodoApi/Dockerfile | 28 +++++++ samples/TodoApi/Domain/Todo.cs | 55 ++++++++++++++ samples/TodoApi/Dtos/TodoDto.cs | 12 +++ samples/TodoApi/Extensions/TodoExtensions.cs | 19 +++++ .../Infrastructure/Db/TodoDbContext.cs | 12 +++ .../20180817091043_InitTodoDb.Designer.cs | 47 ++++++++++++ .../Migrations/20180817091043_InitTodoDb.cs | 33 +++++++++ .../Migrations/TodoDbContextModelSnapshot.cs | 45 +++++++++++ .../TodoApi/NetCoreKit.Samples.TodoAPI.csproj | 29 ++++++++ samples/TodoApi/Program.cs | 34 +++++++++ samples/TodoApi/Startup.cs | 38 ++++++++++ samples/TodoApi/appsettings.Development.json | 18 +++++ samples/TodoApi/appsettings.json | 48 ++++++++++++ samples/TodoApi/v1/TodoController.cs | 74 +++++++++++++++++++ .../TodoApi/v1/UseCases/AddTodo/Payloads.cs | 25 +++++++ .../v1/UseCases/AddTodo/RequestHandler.cs | 28 +++++++ .../v1/UseCases/ClearTodos/Payloads.cs | 13 ++++ .../v1/UseCases/ClearTodos/RequestHandler.cs | 34 +++++++++ .../v1/UseCases/DeleteTodo/Payloads.cs | 15 ++++ .../v1/UseCases/DeleteTodo/RequestHandler.cs | 35 +++++++++ .../TodoApi/v1/UseCases/GetTodo/Payloads.cs | 16 ++++ .../v1/UseCases/GetTodo/RequestHandler.cs | 27 +++++++ .../TodoApi/v1/UseCases/GetTodos/Payloads.cs | 15 ++++ .../v1/UseCases/GetTodos/RequestHandler.cs | 28 +++++++ .../v1/UseCases/UpdateTodo/Payloads.cs | 20 +++++ .../v1/UseCases/UpdateTodo/RequestHandler.cs | 44 +++++++++++ .../IEventHandler.cs | 40 +--------- .../RequestHandlerBase.cs | 20 +++++ .../TxRequestHandlerBase.cs | 23 ++++++ .../CleanArchConfigureService.cs | 5 +- .../DatabaseConfigureService.cs | 33 ++++++++- .../ConfigureServices/MvcConfigureService.cs | 16 +++- .../HealthController.cs | 38 ++++++++++ .../HomeController.cs | 25 +++++++ .../MiniServiceExtensions.cs | 20 +++++ .../Extensions/WebHostExtensions.cs | 20 ++++- .../NetCoreKit.Infrastructure.EfCore.csproj | 1 + .../IExternalSystem.cs | 9 +++ .../NetCoreKit.Infrastructure.csproj | 20 +++++ 43 files changed, 1084 insertions(+), 49 deletions(-) create mode 100644 .dockerignore create mode 100644 samples/TodoApi/.dockerignore create mode 100644 samples/TodoApi/Dockerfile create mode 100644 samples/TodoApi/Domain/Todo.cs create mode 100644 samples/TodoApi/Dtos/TodoDto.cs create mode 100644 samples/TodoApi/Extensions/TodoExtensions.cs create mode 100644 samples/TodoApi/Infrastructure/Db/TodoDbContext.cs create mode 100644 samples/TodoApi/Migrations/20180817091043_InitTodoDb.Designer.cs create mode 100644 samples/TodoApi/Migrations/20180817091043_InitTodoDb.cs create mode 100644 samples/TodoApi/Migrations/TodoDbContextModelSnapshot.cs create mode 100644 samples/TodoApi/NetCoreKit.Samples.TodoAPI.csproj create mode 100644 samples/TodoApi/Program.cs create mode 100644 samples/TodoApi/Startup.cs create mode 100644 samples/TodoApi/appsettings.Development.json create mode 100644 samples/TodoApi/appsettings.json create mode 100644 samples/TodoApi/v1/TodoController.cs create mode 100644 samples/TodoApi/v1/UseCases/AddTodo/Payloads.cs create mode 100644 samples/TodoApi/v1/UseCases/AddTodo/RequestHandler.cs create mode 100644 samples/TodoApi/v1/UseCases/ClearTodos/Payloads.cs create mode 100644 samples/TodoApi/v1/UseCases/ClearTodos/RequestHandler.cs create mode 100644 samples/TodoApi/v1/UseCases/DeleteTodo/Payloads.cs create mode 100644 samples/TodoApi/v1/UseCases/DeleteTodo/RequestHandler.cs create mode 100644 samples/TodoApi/v1/UseCases/GetTodo/Payloads.cs create mode 100644 samples/TodoApi/v1/UseCases/GetTodo/RequestHandler.cs create mode 100644 samples/TodoApi/v1/UseCases/GetTodos/Payloads.cs create mode 100644 samples/TodoApi/v1/UseCases/GetTodos/RequestHandler.cs create mode 100644 samples/TodoApi/v1/UseCases/UpdateTodo/Payloads.cs create mode 100644 samples/TodoApi/v1/UseCases/UpdateTodo/RequestHandler.cs create mode 100644 src/NetCoreKit.Infrastructure.AspNetCore.CleanArch/RequestHandlerBase.cs create mode 100644 src/NetCoreKit.Infrastructure.AspNetCore.CleanArch/TxRequestHandlerBase.cs create mode 100644 src/NetCoreKit.Infrastructure.AspNetCore.Miniservice/HealthController.cs create mode 100644 src/NetCoreKit.Infrastructure.AspNetCore.Miniservice/HomeController.cs create mode 100644 src/NetCoreKit.Infrastructure/IExternalSystem.cs create mode 100644 src/NetCoreKit.Infrastructure/NetCoreKit.Infrastructure.csproj diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..df2e0fe --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.dockerignore +.env +.git +.gitignore +.vs +.vscode +*/bin +*/obj +**/.toolstarget \ No newline at end of file diff --git a/build.cake b/build.cake index 23e339f..1249540 100644 --- a/build.cake +++ b/build.cake @@ -10,6 +10,7 @@ var isWindows = IsRunningOnWindows(); var libs = new List{ "./src/NetCoreKit.Utils/NetCoreKit.Utils.csproj", "./src/NetCoreKit.Domain/NetCoreKit.Domain.csproj", + "./src/NetCoreKit.Infrastructure/NetCoreKit.Infrastructure.csproj", "./src/NetCoreKit.Infrastructure.AspNetCore/NetCoreKit.Infrastructure.AspNetCore.csproj", "./src/NetCoreKit.Infrastructure.AspNetCore.CleanArch/NetCoreKit.Infrastructure.AspNetCore.CleanArch.csproj", "./src/NetCoreKit.Infrastructure.AspNetCore.Miniservice/NetCoreKit.Infrastructure.AspNetCore.Miniservice.csproj", diff --git a/netcore-kit.sln b/netcore-kit.sln index c125ad9..2793857 100644 --- a/netcore-kit.sln +++ b/netcore-kit.sln @@ -39,44 +39,98 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__", "__", "{EE8515E4-F600-4699-A652-FE8E241B9A83}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetCoreKit.Samples.TodoAPI", "samples\TodoApi\NetCoreKit.Samples.TodoAPI.csproj", "{07F7005A-062E-478D-93C0-3B7E1C42389E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetCoreKit.Infrastructure", "src\NetCoreKit.Infrastructure\NetCoreKit.Infrastructure.csproj", "{EE16B7AA-67D5-463E-A24F-6221DBD80A5F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {B9235669-6FFC-4266-B103-851B07EC6E8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B9235669-6FFC-4266-B103-851B07EC6E8E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B9235669-6FFC-4266-B103-851B07EC6E8E}.Debug|x86.ActiveCfg = Debug|Any CPU + {B9235669-6FFC-4266-B103-851B07EC6E8E}.Debug|x86.Build.0 = Debug|Any CPU {B9235669-6FFC-4266-B103-851B07EC6E8E}.Release|Any CPU.ActiveCfg = Release|Any CPU {B9235669-6FFC-4266-B103-851B07EC6E8E}.Release|Any CPU.Build.0 = Release|Any CPU + {B9235669-6FFC-4266-B103-851B07EC6E8E}.Release|x86.ActiveCfg = Release|Any CPU + {B9235669-6FFC-4266-B103-851B07EC6E8E}.Release|x86.Build.0 = Release|Any CPU {CAF26917-0B10-4FB9-BD59-F792D21C2BD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CAF26917-0B10-4FB9-BD59-F792D21C2BD8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CAF26917-0B10-4FB9-BD59-F792D21C2BD8}.Debug|x86.ActiveCfg = Debug|Any CPU + {CAF26917-0B10-4FB9-BD59-F792D21C2BD8}.Debug|x86.Build.0 = Debug|Any CPU {CAF26917-0B10-4FB9-BD59-F792D21C2BD8}.Release|Any CPU.ActiveCfg = Release|Any CPU {CAF26917-0B10-4FB9-BD59-F792D21C2BD8}.Release|Any CPU.Build.0 = Release|Any CPU + {CAF26917-0B10-4FB9-BD59-F792D21C2BD8}.Release|x86.ActiveCfg = Release|Any CPU + {CAF26917-0B10-4FB9-BD59-F792D21C2BD8}.Release|x86.Build.0 = Release|Any CPU {29F1B230-7A1A-43DB-9294-D3A664220D8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {29F1B230-7A1A-43DB-9294-D3A664220D8C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {29F1B230-7A1A-43DB-9294-D3A664220D8C}.Debug|x86.ActiveCfg = Debug|Any CPU + {29F1B230-7A1A-43DB-9294-D3A664220D8C}.Debug|x86.Build.0 = Debug|Any CPU {29F1B230-7A1A-43DB-9294-D3A664220D8C}.Release|Any CPU.ActiveCfg = Release|Any CPU {29F1B230-7A1A-43DB-9294-D3A664220D8C}.Release|Any CPU.Build.0 = Release|Any CPU + {29F1B230-7A1A-43DB-9294-D3A664220D8C}.Release|x86.ActiveCfg = Release|Any CPU + {29F1B230-7A1A-43DB-9294-D3A664220D8C}.Release|x86.Build.0 = Release|Any CPU {F57C8FE9-DC9C-4858-8EA7-6AEF523557A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F57C8FE9-DC9C-4858-8EA7-6AEF523557A4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F57C8FE9-DC9C-4858-8EA7-6AEF523557A4}.Debug|x86.ActiveCfg = Debug|Any CPU + {F57C8FE9-DC9C-4858-8EA7-6AEF523557A4}.Debug|x86.Build.0 = Debug|Any CPU {F57C8FE9-DC9C-4858-8EA7-6AEF523557A4}.Release|Any CPU.ActiveCfg = Release|Any CPU {F57C8FE9-DC9C-4858-8EA7-6AEF523557A4}.Release|Any CPU.Build.0 = Release|Any CPU + {F57C8FE9-DC9C-4858-8EA7-6AEF523557A4}.Release|x86.ActiveCfg = Release|Any CPU + {F57C8FE9-DC9C-4858-8EA7-6AEF523557A4}.Release|x86.Build.0 = Release|Any CPU {E8AA73B0-2BFC-4D9F-8921-CE25A6EE49FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E8AA73B0-2BFC-4D9F-8921-CE25A6EE49FB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E8AA73B0-2BFC-4D9F-8921-CE25A6EE49FB}.Debug|x86.ActiveCfg = Debug|Any CPU + {E8AA73B0-2BFC-4D9F-8921-CE25A6EE49FB}.Debug|x86.Build.0 = Debug|Any CPU {E8AA73B0-2BFC-4D9F-8921-CE25A6EE49FB}.Release|Any CPU.ActiveCfg = Release|Any CPU {E8AA73B0-2BFC-4D9F-8921-CE25A6EE49FB}.Release|Any CPU.Build.0 = Release|Any CPU + {E8AA73B0-2BFC-4D9F-8921-CE25A6EE49FB}.Release|x86.ActiveCfg = Release|Any CPU + {E8AA73B0-2BFC-4D9F-8921-CE25A6EE49FB}.Release|x86.Build.0 = Release|Any CPU {86E10CC4-BAA0-459F-B0C9-3C6CE466DD9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {86E10CC4-BAA0-459F-B0C9-3C6CE466DD9B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {86E10CC4-BAA0-459F-B0C9-3C6CE466DD9B}.Debug|x86.ActiveCfg = Debug|Any CPU + {86E10CC4-BAA0-459F-B0C9-3C6CE466DD9B}.Debug|x86.Build.0 = Debug|Any CPU {86E10CC4-BAA0-459F-B0C9-3C6CE466DD9B}.Release|Any CPU.ActiveCfg = Release|Any CPU {86E10CC4-BAA0-459F-B0C9-3C6CE466DD9B}.Release|Any CPU.Build.0 = Release|Any CPU + {86E10CC4-BAA0-459F-B0C9-3C6CE466DD9B}.Release|x86.ActiveCfg = Release|Any CPU + {86E10CC4-BAA0-459F-B0C9-3C6CE466DD9B}.Release|x86.Build.0 = Release|Any CPU {FA666FF1-5453-481D-BBDD-842F151F9E75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FA666FF1-5453-481D-BBDD-842F151F9E75}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FA666FF1-5453-481D-BBDD-842F151F9E75}.Debug|x86.ActiveCfg = Debug|Any CPU + {FA666FF1-5453-481D-BBDD-842F151F9E75}.Debug|x86.Build.0 = Debug|Any CPU {FA666FF1-5453-481D-BBDD-842F151F9E75}.Release|Any CPU.ActiveCfg = Release|Any CPU {FA666FF1-5453-481D-BBDD-842F151F9E75}.Release|Any CPU.Build.0 = Release|Any CPU + {FA666FF1-5453-481D-BBDD-842F151F9E75}.Release|x86.ActiveCfg = Release|Any CPU + {FA666FF1-5453-481D-BBDD-842F151F9E75}.Release|x86.Build.0 = Release|Any CPU {D6B29A97-1417-48A6-B141-74D57E5C8C16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D6B29A97-1417-48A6-B141-74D57E5C8C16}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D6B29A97-1417-48A6-B141-74D57E5C8C16}.Debug|x86.ActiveCfg = Debug|Any CPU + {D6B29A97-1417-48A6-B141-74D57E5C8C16}.Debug|x86.Build.0 = Debug|Any CPU {D6B29A97-1417-48A6-B141-74D57E5C8C16}.Release|Any CPU.ActiveCfg = Release|Any CPU {D6B29A97-1417-48A6-B141-74D57E5C8C16}.Release|Any CPU.Build.0 = Release|Any CPU + {D6B29A97-1417-48A6-B141-74D57E5C8C16}.Release|x86.ActiveCfg = Release|Any CPU + {D6B29A97-1417-48A6-B141-74D57E5C8C16}.Release|x86.Build.0 = Release|Any CPU + {07F7005A-062E-478D-93C0-3B7E1C42389E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {07F7005A-062E-478D-93C0-3B7E1C42389E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {07F7005A-062E-478D-93C0-3B7E1C42389E}.Debug|x86.ActiveCfg = Debug|Any CPU + {07F7005A-062E-478D-93C0-3B7E1C42389E}.Debug|x86.Build.0 = Debug|Any CPU + {07F7005A-062E-478D-93C0-3B7E1C42389E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {07F7005A-062E-478D-93C0-3B7E1C42389E}.Release|Any CPU.Build.0 = Release|Any CPU + {07F7005A-062E-478D-93C0-3B7E1C42389E}.Release|x86.ActiveCfg = Release|Any CPU + {07F7005A-062E-478D-93C0-3B7E1C42389E}.Release|x86.Build.0 = Release|Any CPU + {EE16B7AA-67D5-463E-A24F-6221DBD80A5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EE16B7AA-67D5-463E-A24F-6221DBD80A5F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EE16B7AA-67D5-463E-A24F-6221DBD80A5F}.Debug|x86.ActiveCfg = Debug|Any CPU + {EE16B7AA-67D5-463E-A24F-6221DBD80A5F}.Debug|x86.Build.0 = Debug|Any CPU + {EE16B7AA-67D5-463E-A24F-6221DBD80A5F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EE16B7AA-67D5-463E-A24F-6221DBD80A5F}.Release|Any CPU.Build.0 = Release|Any CPU + {EE16B7AA-67D5-463E-A24F-6221DBD80A5F}.Release|x86.ActiveCfg = Release|Any CPU + {EE16B7AA-67D5-463E-A24F-6221DBD80A5F}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -91,6 +145,8 @@ Global {FA666FF1-5453-481D-BBDD-842F151F9E75} = {6CF50533-EBD6-47B1-9AD9-9EBBFF3AC887} {D6B29A97-1417-48A6-B141-74D57E5C8C16} = {6CF50533-EBD6-47B1-9AD9-9EBBFF3AC887} {A1CCC105-67B3-45AE-9A74-DD6716521708} = {EE8515E4-F600-4699-A652-FE8E241B9A83} + {07F7005A-062E-478D-93C0-3B7E1C42389E} = {92D6FD73-B95C-4CBB-A48E-047672EB4E77} + {EE16B7AA-67D5-463E-A24F-6221DBD80A5F} = {6CF50533-EBD6-47B1-9AD9-9EBBFF3AC887} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B9325AE8-21A8-4D46-9B04-38BD2BC35C0F} diff --git a/samples/TodoApi/.dockerignore b/samples/TodoApi/.dockerignore new file mode 100644 index 0000000..2036336 --- /dev/null +++ b/samples/TodoApi/.dockerignore @@ -0,0 +1,5 @@ +* +!src +!*.sln +**/bin +**/obj \ No newline at end of file diff --git a/samples/TodoApi/Dockerfile b/samples/TodoApi/Dockerfile new file mode 100644 index 0000000..f398127 --- /dev/null +++ b/samples/TodoApi/Dockerfile @@ -0,0 +1,28 @@ +FROM microsoft/dotnet:2.1.2-aspnetcore-runtime-alpine AS base +WORKDIR /app + +ARG service_version +ENV SERVICE_VERSION ${service_version:-0.0.1} + +ARG api_version +ENV API_VERSION ${api_version:-1.0} + +ENV ASPNETCORE_URLS http://+:5001 +EXPOSE 5001 + +FROM microsoft/dotnet:2.1.302-sdk-alpine AS build +WORKDIR /src +COPY . . + +WORKDIR /src/src/samples + +RUN dotnet restore -nowarn:msb3202,nu1503 +RUN dotnet build --no-restore -c Release -o /app + +FROM build AS publish +RUN dotnet publish --no-restore -c Release -o /app + +FROM base AS final +WORKDIR /app +COPY --from=publish /app . +ENTRYPOINT ["dotnet", "Company.Application.samples.dll"] \ No newline at end of file diff --git a/samples/TodoApi/Domain/Todo.cs b/samples/TodoApi/Domain/Todo.cs new file mode 100644 index 0000000..83ccff4 --- /dev/null +++ b/samples/TodoApi/Domain/Todo.cs @@ -0,0 +1,55 @@ +using System; +using System.ComponentModel.DataAnnotations; +using NetCoreKit.Domain; + +namespace NetCoreKit.Samples.TodoAPI.Domain +{ + public sealed class Todo : EntityBase + { + internal Todo(string title) + : this(Guid.NewGuid(), title) + { + } + + public Todo(Guid id, string title) : base(id) + { + Title = title; + } + + public static Todo Load(string title) + { + return new Todo(title); + } + + public static Todo Load(Guid id, string title) + { + return new Todo(id, title); + } + + public int? Order { get; private set; } = 1; + [Required] public string Title { get; private set; } + public bool? Completed { get; private set; } = false; + + public Todo ChangeTitle(string title) + { + if(string.IsNullOrEmpty(title)) + throw new DomainException("Order is null or empty."); + Title = title; + return this; + } + + public Todo ChangeOrder(int order) + { + if (order <= 0) + throw new DomainException("Order could be greater than zero."); + Order = order; + return this; + } + + public Todo ChangeToCompleted() + { + Completed = true; + return this; + } + } +} diff --git a/samples/TodoApi/Dtos/TodoDto.cs b/samples/TodoApi/Dtos/TodoDto.cs new file mode 100644 index 0000000..bc2117c --- /dev/null +++ b/samples/TodoApi/Dtos/TodoDto.cs @@ -0,0 +1,12 @@ +using System; + +namespace NetCoreKit.Samples.TodoAPI.Dtos +{ + public class TodoDto + { + public Guid Id { get; set; } + public string Title { get; set; } + public int Order { get; set; } + public bool Completed { get; set; } + } +} diff --git a/samples/TodoApi/Extensions/TodoExtensions.cs b/samples/TodoApi/Extensions/TodoExtensions.cs new file mode 100644 index 0000000..4ec13c7 --- /dev/null +++ b/samples/TodoApi/Extensions/TodoExtensions.cs @@ -0,0 +1,19 @@ +using NetCoreKit.Samples.TodoAPI.Domain; +using NetCoreKit.Samples.TodoAPI.Dtos; + +namespace NetCoreKit.Samples.TodoAPI.Extensions +{ + public static class TodoExtensions + { + public static TodoDto ToDto(this Todo todo) + { + return new TodoDto + { + Id = todo.Id, + Title = todo.Title, + Completed = todo.Completed ?? false, + Order = todo.Order ?? 1 + }; + } + } +} diff --git a/samples/TodoApi/Infrastructure/Db/TodoDbContext.cs b/samples/TodoApi/Infrastructure/Db/TodoDbContext.cs new file mode 100644 index 0000000..23bb57b --- /dev/null +++ b/samples/TodoApi/Infrastructure/Db/TodoDbContext.cs @@ -0,0 +1,12 @@ +using Microsoft.EntityFrameworkCore; +using NetCoreKit.Infrastructure.EfCore.Db; + +namespace NetCoreKit.Samples.TodoAPI.Infrastructure.Db +{ + public class TodoDbContext : ApplicationDbContext + { + public TodoDbContext(DbContextOptions options) : base(options) + { + } + } +} \ No newline at end of file diff --git a/samples/TodoApi/Migrations/20180817091043_InitTodoDb.Designer.cs b/samples/TodoApi/Migrations/20180817091043_InitTodoDb.Designer.cs new file mode 100644 index 0000000..c69e104 --- /dev/null +++ b/samples/TodoApi/Migrations/20180817091043_InitTodoDb.Designer.cs @@ -0,0 +1,47 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NetCoreKit.Samples.TodoAPI.Infrastructure.Db; + +namespace NetCoreKit.Samples.TodoAPI.Migrations +{ + [DbContext(typeof(TodoDbContext))] + [Migration("20180817091043_InitTodoDb")] + partial class InitTodoDb + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.1-rtm-30846") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("NetCoreKit.Samples.TodoAPI.Domain.Todo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Completed"); + + b.Property("Created"); + + b.Property("Order"); + + b.Property("Title") + .IsRequired(); + + b.Property("Updated"); + + b.HasKey("Id"); + + b.ToTable("Todos"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/samples/TodoApi/Migrations/20180817091043_InitTodoDb.cs b/samples/TodoApi/Migrations/20180817091043_InitTodoDb.cs new file mode 100644 index 0000000..40b590d --- /dev/null +++ b/samples/TodoApi/Migrations/20180817091043_InitTodoDb.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace NetCoreKit.Samples.TodoAPI.Migrations +{ + public partial class InitTodoDb : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Todos", + columns: table => new + { + Id = table.Column(nullable: false), + Created = table.Column(nullable: false), + Updated = table.Column(nullable: false), + Order = table.Column(nullable: true), + Title = table.Column(nullable: false), + Completed = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Todos", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Todos"); + } + } +} diff --git a/samples/TodoApi/Migrations/TodoDbContextModelSnapshot.cs b/samples/TodoApi/Migrations/TodoDbContextModelSnapshot.cs new file mode 100644 index 0000000..57be063 --- /dev/null +++ b/samples/TodoApi/Migrations/TodoDbContextModelSnapshot.cs @@ -0,0 +1,45 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NetCoreKit.Samples.TodoAPI.Infrastructure.Db; + +namespace NetCoreKit.Samples.TodoAPI.Migrations +{ + [DbContext(typeof(TodoDbContext))] + partial class TodoDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.1-rtm-30846") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("NetCoreKit.Samples.TodoAPI.Domain.Todo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Completed"); + + b.Property("Created"); + + b.Property("Order"); + + b.Property("Title") + .IsRequired(); + + b.Property("Updated"); + + b.HasKey("Id"); + + b.ToTable("Todos"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/samples/TodoApi/NetCoreKit.Samples.TodoAPI.csproj b/samples/TodoApi/NetCoreKit.Samples.TodoAPI.csproj new file mode 100644 index 0000000..6ebc287 --- /dev/null +++ b/samples/TodoApi/NetCoreKit.Samples.TodoAPI.csproj @@ -0,0 +1,29 @@ + + + + netcoreapp2.1 + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/TodoApi/Program.cs b/samples/TodoApi/Program.cs new file mode 100644 index 0000000..ef52ab7 --- /dev/null +++ b/samples/TodoApi/Program.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using NetCoreKit.Infrastructure.EfCore.Extensions; +using NetCoreKit.Samples.TodoAPI.Infrastructure.Db; + +namespace NetCoreKit.Samples.TodoAPI +{ + public class Program + { + public static void Main(string[] args) + { + var webHost = CreateWebHostBuilder(args).Build(); + var config = webHost.Services.GetService(); + if (!config.GetValue("SqlDatabase:Enabled")) + { + webHost.Run(); + return; + } + + var env = webHost.Services.GetService(); + if (env.IsDevelopment()) webHost = webHost.RegisterDbContext(); + + webHost.Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) + { + return WebHost.CreateDefaultBuilder(args) + .UseStartup(); + } + } +} diff --git a/samples/TodoApi/Startup.cs b/samples/TodoApi/Startup.cs new file mode 100644 index 0000000..e781e99 --- /dev/null +++ b/samples/TodoApi/Startup.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Reflection; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using NetCoreKit.Infrastructure.AspNetCore.Miniservice; +using NetCoreKit.Infrastructure.EfCore.SqlServer; +using NetCoreKit.Samples.TodoAPI.Infrastructure.Db; + +namespace NetCoreKit.Samples.TodoAPI +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + var assemblies = new HashSet + { + typeof(Startup).GetTypeInfo().Assembly, + typeof(MiniServiceExtensions).GetTypeInfo().Assembly + }; + + var serviceParams = new ServiceParams + { + {"assemblies", assemblies} + }; + + services.AddScoped(sp => serviceParams); + services.AddEfCoreSqlServer(); + services.AddMiniService(); + services.AddExternalSystemHealthChecks(); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + app.UseMiniService(); + } + } +} diff --git a/samples/TodoApi/appsettings.Development.json b/samples/TodoApi/appsettings.Development.json new file mode 100644 index 0000000..d3dfd3b --- /dev/null +++ b/samples/TodoApi/appsettings.Development.json @@ -0,0 +1,18 @@ +{ + "ConnectionStrings": { + "mssqldb": "Server=tcp:127.0.0.1,1433;Database=maindb;User Id=cs;Password=P@ssw0rd;" + }, + "Hosts": { + "Externals": { + "CurrentUri": "http://localhost:5001" + } + }, + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/samples/TodoApi/appsettings.json b/samples/TodoApi/appsettings.json new file mode 100644 index 0000000..7a2e5e9 --- /dev/null +++ b/samples/TodoApi/appsettings.json @@ -0,0 +1,48 @@ +{ + "API_VERSION": "1.0", + "SERVICE_VERSION": "0.0.1", + "EnableAuthN": false, + "ConnectionStrings": { + "mssqldb": "Server=tcp:{0},{1};Database={2};User Id={3};Password={4};" + }, + "EfCore": { + "FullyQualifiedPrefix": "samples.*", + "ShortyQualifiedPrefix": "samples" + }, + "k8s": { + "mssqldb": { + "Host": "TODO_DB_SERVICE_HOST", + "Port": "TODO_DB_SERVICE_PORT", + "Database": "maindb", + "UserName": "cs", + "Password": "P@ssw0rd" + } + }, + "Hosts": { + "BasePath": "/todo/", + "Externals": { + "CurrentUri": "http://api.dotnetkit.local/todo" + + } + }, + "SqlDatabase": { + "Enabled": true + }, + "OpenApi": { + "Enabled": true, + "EnabledUI": true + }, + "Logging": { + "IncludeScopes": false, + "Debug": { + "LogLevel": { + "Default": "Warning" + } + }, + "Console": { + "LogLevel": { + "Default": "Warning" + } + } + } +} diff --git a/samples/TodoApi/v1/TodoController.cs b/samples/TodoApi/v1/TodoController.cs new file mode 100644 index 0000000..e9daa22 --- /dev/null +++ b/samples/TodoApi/v1/TodoController.cs @@ -0,0 +1,74 @@ +using System; +using System.Reactive.Linq; +using System.Threading.Tasks; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using NetCoreKit.Infrastructure.AspNetCore; +using NetCoreKit.Infrastructure.AspNetCore.CleanArch; +using NetCoreKit.Samples.TodoAPI.v1.UseCases.AddTodo; +using NetCoreKit.Samples.TodoAPI.v1.UseCases.ClearTodos; +using NetCoreKit.Samples.TodoAPI.v1.UseCases.DeleteTodo; +using NetCoreKit.Samples.TodoAPI.v1.UseCases.GetTodo; +using NetCoreKit.Samples.TodoAPI.v1.UseCases.GetTodos; +using NetCoreKit.Samples.TodoAPI.v1.UseCases.UpdateTodo; + +namespace NetCoreKit.Samples.TodoAPI.v1 +{ + [ApiVersion("1.0")] + [Route("api/todos")] + public class TodoController : EvtControllerBase + { + public TodoController(IMediator eventor) : base(eventor) + { + } + + [HttpGet] + public async Task Get() + { + return await Eventor.SendStream( + new GetTodosRequest(), + x => x.Result); + } + + [HttpGet("{todoId:guid}")] + public async Task Get(Guid todoId) + { + return await Eventor.SendStream( + new GetTodoRequest {Id = todoId}, + x => x.Result); + } + + [HttpPost] + public async Task Post(AddTodoRequest request) + { + return await Eventor.SendStream( + request, + x => x.Result); + } + + [HttpDelete("{todoId:guid}")] + public async Task Delete(Guid todoId) + { + return await Eventor.SendStream( + new DeleteTodoRequest {Id = todoId}, + x => x.Result); + } + + [HttpDelete] + public async Task Clear() + { + return await Eventor.SendStream( + new ClearTodosRequest(), + x => x.Result); + } + + [HttpPut("{todoId:guid}")] + public async Task Clear(Guid todoId, UpdateTodoRequest request) + { + request.Id = todoId; + return await Eventor.SendStream( + request, + x => x.Result); + } + } +} diff --git a/samples/TodoApi/v1/UseCases/AddTodo/Payloads.cs b/samples/TodoApi/v1/UseCases/AddTodo/Payloads.cs new file mode 100644 index 0000000..ffc74ac --- /dev/null +++ b/samples/TodoApi/v1/UseCases/AddTodo/Payloads.cs @@ -0,0 +1,25 @@ +using System.ComponentModel.DataAnnotations; +using MediatR; +using NetCoreKit.Samples.TodoAPI.Dtos; + +namespace NetCoreKit.Samples.TodoAPI.v1.UseCases.AddTodo +{ + public class AddTodoRequest : IRequest + { + public AddTodoRequest() + { + Completed = false; + Order = 1; + Title = "sample todo"; + } + + public int? Order { get; set; } + [Required] public string Title { get; set; } + public bool? Completed { get; set; } + } + + public class AddTodoResponse + { + public TodoDto Result { get; set; } + } +} diff --git a/samples/TodoApi/v1/UseCases/AddTodo/RequestHandler.cs b/samples/TodoApi/v1/UseCases/AddTodo/RequestHandler.cs new file mode 100644 index 0000000..217327e --- /dev/null +++ b/samples/TodoApi/v1/UseCases/AddTodo/RequestHandler.cs @@ -0,0 +1,28 @@ +using System.Threading; +using System.Threading.Tasks; +using NetCoreKit.Domain; +using NetCoreKit.Infrastructure.AspNetCore.CleanArch; +using NetCoreKit.Samples.TodoAPI.Domain; +using NetCoreKit.Samples.TodoAPI.Extensions; + +namespace NetCoreKit.Samples.TodoAPI.v1.UseCases.AddTodo +{ + public class RequestHandler : TxRequestHandlerBase + { + public RequestHandler(IUnitOfWorkAsync uow, IQueryRepositoryFactory queryRepositoryFactory) + : base(uow, queryRepositoryFactory) + { + } + + public override async Task Handle(AddTodoRequest request, CancellationToken cancellationToken) + { + var todoRepository = UnitOfWork.Repository(); + + var todo = Todo.Load(request.Title); + var result = await todoRepository.AddAsync(todo); + + await UnitOfWork.SaveChangesAsync(cancellationToken); + return new AddTodoResponse {Result = result.ToDto()}; + } + } +} diff --git a/samples/TodoApi/v1/UseCases/ClearTodos/Payloads.cs b/samples/TodoApi/v1/UseCases/ClearTodos/Payloads.cs new file mode 100644 index 0000000..95cd786 --- /dev/null +++ b/samples/TodoApi/v1/UseCases/ClearTodos/Payloads.cs @@ -0,0 +1,13 @@ +using MediatR; + +namespace NetCoreKit.Samples.TodoAPI.v1.UseCases.ClearTodos +{ + public class ClearTodosRequest : IRequest + { + } + + public class ClearTodosResponse + { + public bool Result { get; set; } = true; + } +} diff --git a/samples/TodoApi/v1/UseCases/ClearTodos/RequestHandler.cs b/samples/TodoApi/v1/UseCases/ClearTodos/RequestHandler.cs new file mode 100644 index 0000000..f2617e5 --- /dev/null +++ b/samples/TodoApi/v1/UseCases/ClearTodos/RequestHandler.cs @@ -0,0 +1,34 @@ +using System.Threading; +using System.Threading.Tasks; +using NetCoreKit.Domain; +using NetCoreKit.Infrastructure.AspNetCore.CleanArch; +using NetCoreKit.Infrastructure.EfCore.Extensions; + +namespace NetCoreKit.Samples.TodoAPI.v1.UseCases.ClearTodos +{ + public class RequestHandler : TxRequestHandlerBase + { + public RequestHandler(IUnitOfWorkAsync uow, IQueryRepositoryFactory queryRepositoryFactory) + : base(uow, queryRepositoryFactory) + { + } + + public override async Task Handle(ClearTodosRequest request, + CancellationToken cancellationToken) + { + var commandRepository = UnitOfWork.Repository(); + var queryRepository = QueryRepositoryFactory.QueryEfRepository(); + + var todos = await queryRepository.ListAsync(); + if (todos == null || todos.Count <= 0) return new ClearTodosResponse(); + //TODO: need to have a ClearAll method in CommandRepository + foreach (var todo in todos) + { + await commandRepository.DeleteAsync(todo); + } + + await UnitOfWork.SaveChangesAsync(cancellationToken); + return new ClearTodosResponse(); + } + } +} diff --git a/samples/TodoApi/v1/UseCases/DeleteTodo/Payloads.cs b/samples/TodoApi/v1/UseCases/DeleteTodo/Payloads.cs new file mode 100644 index 0000000..a0fed45 --- /dev/null +++ b/samples/TodoApi/v1/UseCases/DeleteTodo/Payloads.cs @@ -0,0 +1,15 @@ +using System; +using MediatR; + +namespace NetCoreKit.Samples.TodoAPI.v1.UseCases.DeleteTodo +{ + public class DeleteTodoRequest : IRequest + { + public Guid Id { get; set; } + } + + public class DeleteTodoResponse + { + public Guid Result { get; set; } + } +} \ No newline at end of file diff --git a/samples/TodoApi/v1/UseCases/DeleteTodo/RequestHandler.cs b/samples/TodoApi/v1/UseCases/DeleteTodo/RequestHandler.cs new file mode 100644 index 0000000..2dec1fb --- /dev/null +++ b/samples/TodoApi/v1/UseCases/DeleteTodo/RequestHandler.cs @@ -0,0 +1,35 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using NetCoreKit.Domain; +using NetCoreKit.Infrastructure.AspNetCore.CleanArch; +using NetCoreKit.Infrastructure.EfCore.Extensions; + +namespace NetCoreKit.Samples.TodoAPI.v1.UseCases.DeleteTodo +{ + public class RequestHandler : TxRequestHandlerBase + { + public RequestHandler(IUnitOfWorkAsync uow, IQueryRepositoryFactory queryRepositoryFactory) + : base(uow, queryRepositoryFactory) + { + } + + public override async Task Handle(DeleteTodoRequest request, + CancellationToken cancellationToken) + { + var commandRepository = UnitOfWork.Repository(); + var queryRepository = QueryRepositoryFactory.QueryEfRepository(); + + var todo = await queryRepository.GetByIdAsync(request.Id); + if (todo == null) + { + throw new Exception($"Could not find item #{request.Id}."); + } + + await commandRepository.DeleteAsync(todo); + + await UnitOfWork.SaveChangesAsync(cancellationToken); + return new DeleteTodoResponse {Result = todo.Id}; + } + } +} diff --git a/samples/TodoApi/v1/UseCases/GetTodo/Payloads.cs b/samples/TodoApi/v1/UseCases/GetTodo/Payloads.cs new file mode 100644 index 0000000..439a436 --- /dev/null +++ b/samples/TodoApi/v1/UseCases/GetTodo/Payloads.cs @@ -0,0 +1,16 @@ +using System; +using MediatR; +using NetCoreKit.Samples.TodoAPI.Dtos; + +namespace NetCoreKit.Samples.TodoAPI.v1.UseCases.GetTodo +{ + public class GetTodoRequest : IRequest + { + public Guid Id { get; set; } + } + + public class GetTodoResponse + { + public TodoDto Result { get; set; } + } +} \ No newline at end of file diff --git a/samples/TodoApi/v1/UseCases/GetTodo/RequestHandler.cs b/samples/TodoApi/v1/UseCases/GetTodo/RequestHandler.cs new file mode 100644 index 0000000..bd8bc81 --- /dev/null +++ b/samples/TodoApi/v1/UseCases/GetTodo/RequestHandler.cs @@ -0,0 +1,27 @@ +using System.Threading; +using System.Threading.Tasks; +using NetCoreKit.Domain; +using NetCoreKit.Infrastructure.AspNetCore.CleanArch; +using NetCoreKit.Infrastructure.EfCore.Extensions; +using NetCoreKit.Samples.TodoAPI.Extensions; + +namespace NetCoreKit.Samples.TodoAPI.v1.UseCases.GetTodo +{ + public class RequestHandler : RequestHandlerBase + { + public RequestHandler(IQueryRepositoryFactory queryRepositoryFactory) + : base(queryRepositoryFactory) + { + } + + public override async Task Handle(GetTodoRequest request, CancellationToken cancellationToken) + { + var queryRepository = QueryRepositoryFactory.QueryEfRepository(); + var result = await queryRepository.GetByIdAsync(request.Id); + return new GetTodoResponse + { + Result = result.ToDto() + }; + } + } +} diff --git a/samples/TodoApi/v1/UseCases/GetTodos/Payloads.cs b/samples/TodoApi/v1/UseCases/GetTodos/Payloads.cs new file mode 100644 index 0000000..9cbd71e --- /dev/null +++ b/samples/TodoApi/v1/UseCases/GetTodos/Payloads.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using MediatR; +using NetCoreKit.Samples.TodoAPI.Dtos; + +namespace NetCoreKit.Samples.TodoAPI.v1.UseCases.GetTodos +{ + public class GetTodosRequest : IRequest + { + } + + public class GetTodosResponse + { + public List Result { get; set; } + } +} \ No newline at end of file diff --git a/samples/TodoApi/v1/UseCases/GetTodos/RequestHandler.cs b/samples/TodoApi/v1/UseCases/GetTodos/RequestHandler.cs new file mode 100644 index 0000000..e58a69e --- /dev/null +++ b/samples/TodoApi/v1/UseCases/GetTodos/RequestHandler.cs @@ -0,0 +1,28 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using NetCoreKit.Domain; +using NetCoreKit.Infrastructure.AspNetCore.CleanArch; +using NetCoreKit.Infrastructure.EfCore.Extensions; +using NetCoreKit.Samples.TodoAPI.Extensions; + +namespace NetCoreKit.Samples.TodoAPI.v1.UseCases.GetTodos +{ + public class RequestHandler : RequestHandlerBase + { + public RequestHandler(IQueryRepositoryFactory queryRepositoryFactory) + : base(queryRepositoryFactory) + { + } + + public override async Task Handle(GetTodosRequest request, CancellationToken cancellationToken) + { + var queryRepository = QueryRepositoryFactory.QueryEfRepository(); + var result = await queryRepository.ListAsync(); + return new GetTodosResponse + { + Result = result.Select(x => x.ToDto()).ToList() + }; + } + } +} diff --git a/samples/TodoApi/v1/UseCases/UpdateTodo/Payloads.cs b/samples/TodoApi/v1/UseCases/UpdateTodo/Payloads.cs new file mode 100644 index 0000000..2854e1a --- /dev/null +++ b/samples/TodoApi/v1/UseCases/UpdateTodo/Payloads.cs @@ -0,0 +1,20 @@ +using System; +using System.ComponentModel.DataAnnotations; +using MediatR; +using NetCoreKit.Samples.TodoAPI.Dtos; + +namespace NetCoreKit.Samples.TodoAPI.v1.UseCases.UpdateTodo +{ + public class UpdateTodoRequest : IRequest + { + public Guid Id { get; set; } + public int? Order { get; set; } = 1; + [Required] public string Title { get; set; } + public bool? Completed { get; set; } = false; + } + + public class UpdateTodoResponse + { + public TodoDto Result { get; set; } + } +} diff --git a/samples/TodoApi/v1/UseCases/UpdateTodo/RequestHandler.cs b/samples/TodoApi/v1/UseCases/UpdateTodo/RequestHandler.cs new file mode 100644 index 0000000..073c93d --- /dev/null +++ b/samples/TodoApi/v1/UseCases/UpdateTodo/RequestHandler.cs @@ -0,0 +1,44 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using NetCoreKit.Domain; +using NetCoreKit.Infrastructure.AspNetCore.CleanArch; +using NetCoreKit.Infrastructure.EfCore.Extensions; +using NetCoreKit.Samples.TodoAPI.Extensions; + +namespace NetCoreKit.Samples.TodoAPI.v1.UseCases.UpdateTodo +{ + public class RequestHandler : TxRequestHandlerBase + { + public RequestHandler(IUnitOfWorkAsync uow, IQueryRepositoryFactory queryRepositoryFactory) + : base(uow, queryRepositoryFactory) + { + } + + public override async Task Handle(UpdateTodoRequest request, + CancellationToken cancellationToken) + { + var commandRepository = UnitOfWork.Repository(); + var queryRepository = QueryRepositoryFactory.QueryEfRepository(); + + var todo = await queryRepository.GetByIdAsync(request.Id); + if (todo == null) + { + throw new Exception($"Could not find item #{request.Id}."); + } + + todo.ChangeTitle(request.Title) + .ChangeOrder(request.Order ?? 1); + + if (request.Completed.HasValue && request.Completed.Value == true) + { + todo.ChangeToCompleted(); + } + + var updated = await commandRepository.UpdateAsync(todo); + await UnitOfWork.SaveChangesAsync(cancellationToken); + + return new UpdateTodoResponse {Result = updated.ToDto()}; + } + } +} diff --git a/src/NetCoreKit.Infrastructure.AspNetCore.CleanArch/IEventHandler.cs b/src/NetCoreKit.Infrastructure.AspNetCore.CleanArch/IEventHandler.cs index 81ea8ab..3ac4334 100644 --- a/src/NetCoreKit.Infrastructure.AspNetCore.CleanArch/IEventHandler.cs +++ b/src/NetCoreKit.Infrastructure.AspNetCore.CleanArch/IEventHandler.cs @@ -1,5 +1,3 @@ -using System.Threading; -using System.Threading.Tasks; using MediatR; using NetCoreKit.Domain; @@ -9,45 +7,11 @@ public interface IEventHandler : IRequestHandler { IQueryRepositoryFactory QueryRepositoryFactory { get; } - IUnitOfWorkAsync UnitOfWork { get; } } - public abstract class TxRequestHandlerBase : IEventHandler + public interface ITxEventHandler : IEventHandler where TRequest : IRequest { - protected TxRequestHandlerBase(IUnitOfWorkAsync uow, IQueryRepositoryFactory queryRepositoryFactory) - { - QueryRepositoryFactory = queryRepositoryFactory; - UnitOfWork = uow; - } - - public IQueryRepositoryFactory QueryRepositoryFactory { get; } - - public IUnitOfWorkAsync UnitOfWork { get; } - - public async Task Handle(TRequest request, CancellationToken cancellationToken) - { - var result = await TxHandle(request, cancellationToken); - await UnitOfWork.SaveChangesAsync(cancellationToken); - return result; - } - - public abstract Task TxHandle(TRequest request, CancellationToken cancellationToken); - } - - public abstract class RequestHandlerBase : IEventHandler - where TRequest : IRequest - { - protected RequestHandlerBase(IUnitOfWorkAsync uow, IQueryRepositoryFactory queryRepositoryFactory) - { - QueryRepositoryFactory = queryRepositoryFactory; - UnitOfWork = uow; - } - - public IQueryRepositoryFactory QueryRepositoryFactory { get; } - - public IUnitOfWorkAsync UnitOfWork { get; } - - public abstract Task Handle(TRequest request, CancellationToken cancellationToken); + IUnitOfWorkAsync UnitOfWork { get; } } } diff --git a/src/NetCoreKit.Infrastructure.AspNetCore.CleanArch/RequestHandlerBase.cs b/src/NetCoreKit.Infrastructure.AspNetCore.CleanArch/RequestHandlerBase.cs new file mode 100644 index 0000000..7a22007 --- /dev/null +++ b/src/NetCoreKit.Infrastructure.AspNetCore.CleanArch/RequestHandlerBase.cs @@ -0,0 +1,20 @@ +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using NetCoreKit.Domain; + +namespace NetCoreKit.Infrastructure.AspNetCore.CleanArch +{ + public abstract class RequestHandlerBase : IEventHandler + where TRequest : IRequest + { + protected RequestHandlerBase(IQueryRepositoryFactory queryRepositoryFactory) + { + QueryRepositoryFactory = queryRepositoryFactory; + } + + public IQueryRepositoryFactory QueryRepositoryFactory { get; } + + public abstract Task Handle(TRequest request, CancellationToken cancellationToken); + } +} diff --git a/src/NetCoreKit.Infrastructure.AspNetCore.CleanArch/TxRequestHandlerBase.cs b/src/NetCoreKit.Infrastructure.AspNetCore.CleanArch/TxRequestHandlerBase.cs new file mode 100644 index 0000000..981229f --- /dev/null +++ b/src/NetCoreKit.Infrastructure.AspNetCore.CleanArch/TxRequestHandlerBase.cs @@ -0,0 +1,23 @@ +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using NetCoreKit.Domain; + +namespace NetCoreKit.Infrastructure.AspNetCore.CleanArch +{ + public abstract class TxRequestHandlerBase : ITxEventHandler + where TRequest : IRequest + { + protected TxRequestHandlerBase(IUnitOfWorkAsync uow, IQueryRepositoryFactory queryRepositoryFactory) + { + QueryRepositoryFactory = queryRepositoryFactory; + UnitOfWork = uow; + } + + public IQueryRepositoryFactory QueryRepositoryFactory { get; } + + public IUnitOfWorkAsync UnitOfWork { get; } + + public abstract Task Handle(TRequest request, CancellationToken cancellationToken); + } +} diff --git a/src/NetCoreKit.Infrastructure.AspNetCore.Miniservice/ConfigureServices/CleanArchConfigureService.cs b/src/NetCoreKit.Infrastructure.AspNetCore.Miniservice/ConfigureServices/CleanArchConfigureService.cs index ede0d29..e9c9471 100644 --- a/src/NetCoreKit.Infrastructure.AspNetCore.Miniservice/ConfigureServices/CleanArchConfigureService.cs +++ b/src/NetCoreKit.Infrastructure.AspNetCore.Miniservice/ConfigureServices/CleanArchConfigureService.cs @@ -14,9 +14,10 @@ public void Configure(IServiceCollection services) { var svcProvider = services.BuildServiceProvider(); var serviceParams = svcProvider.GetRequiredService(); - var assemblies = serviceParams["assemblies"] as HashSet; - assemblies.Add(typeof(MiniServiceExtensions).Assembly); + if (!(serviceParams["assemblies"] is HashSet assemblies)) return; + + assemblies.Add(typeof(MiniServiceExtensions).Assembly); services.AddMediatR(assemblies.ToArray()); services.Scan( scanner => scanner diff --git a/src/NetCoreKit.Infrastructure.AspNetCore.Miniservice/ConfigureServices/DatabaseConfigureService.cs b/src/NetCoreKit.Infrastructure.AspNetCore.Miniservice/ConfigureServices/DatabaseConfigureService.cs index 40d6953..0f17c58 100644 --- a/src/NetCoreKit.Infrastructure.AspNetCore.Miniservice/ConfigureServices/DatabaseConfigureService.cs +++ b/src/NetCoreKit.Infrastructure.AspNetCore.Miniservice/ConfigureServices/DatabaseConfigureService.cs @@ -1,6 +1,8 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -21,25 +23,50 @@ public void Configure(IServiceCollection services) where TDbContext var svcProvider = services.BuildServiceProvider(); var config = svcProvider.GetRequiredService(); + + if (!config.GetValue("SqlDatabase:Enabled")) + { + return; + } + var serviceParams = svcProvider.GetRequiredService(); var extendOptionsBuilder = svcProvider.GetRequiredService(); var connStringFactory = svcProvider.GetRequiredService(); - //TODO: refactor it - var fisrtAssembly = (serviceParams["assemblies"] as HashSet).FirstOrDefault(); + //TODO: refactor it + var assemblies = serviceParams["assemblies"] as HashSet; + var fisrtAssembly = assemblies?.FirstOrDefault(); services.AddOptions() .Configure(config.GetSection("EfCore")); void OptionsBuilderAction(DbContextOptionsBuilder o) { - extendOptionsBuilder.Extend(o, connStringFactory, fisrtAssembly.GetName().Name); + extendOptionsBuilder.Extend(o, connStringFactory, fisrtAssembly?.GetName().Name); } services.AddDbContextPool(OptionsBuilderAction); services.AddSingleton(); services.AddSingleton(resolver => resolver.GetRequiredService()); services.AddEfCore(); + + // healthcheck and migration automatically + services.AddSingleton(); + } + } + + public class DbHealthCheckAndMigration : IExternalSystem + { + private readonly IServiceProvider _svcProvider; + + public DbHealthCheckAndMigration(IServiceProvider svcProvider) + { + _svcProvider = svcProvider; + } + + public Task Connect() + { + return Task.Run(() => _svcProvider.MigrateDbContext() != null); } } } diff --git a/src/NetCoreKit.Infrastructure.AspNetCore.Miniservice/ConfigureServices/MvcConfigureService.cs b/src/NetCoreKit.Infrastructure.AspNetCore.Miniservice/ConfigureServices/MvcConfigureService.cs index 26c74e4..e248d70 100644 --- a/src/NetCoreKit.Infrastructure.AspNetCore.Miniservice/ConfigureServices/MvcConfigureService.cs +++ b/src/NetCoreKit.Infrastructure.AspNetCore.Miniservice/ConfigureServices/MvcConfigureService.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; +using System.Reflection; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; @@ -9,9 +11,17 @@ public class MvcConfigureService : IBasicConfigureServices public void Configure(IServiceCollection services) { - services - .AddMvc() - .SetCompatibilityVersion(CompatibilityVersion.Version_2_1); + var svcProvider = services.BuildServiceProvider(); + var serviceParams = svcProvider.GetRequiredService(); + + var mvcBuilder = services.AddMvc(); + if (serviceParams["assemblies"] is HashSet assemblies && assemblies.Count > 0) + foreach (var assembly in assemblies) + { + mvcBuilder = mvcBuilder.AddApplicationPart(assembly); + } + + mvcBuilder.SetCompatibilityVersion(CompatibilityVersion.Version_2_1); } } } diff --git a/src/NetCoreKit.Infrastructure.AspNetCore.Miniservice/HealthController.cs b/src/NetCoreKit.Infrastructure.AspNetCore.Miniservice/HealthController.cs new file mode 100644 index 0000000..e568ed5 --- /dev/null +++ b/src/NetCoreKit.Infrastructure.AspNetCore.Miniservice/HealthController.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +namespace NetCoreKit.Infrastructure.AspNetCore.Miniservice +{ + [Route("")] + [ApiVersionNeutral] + [ApiExplorerSettings(IgnoreApi = true)] + public class HealthController : Controller + { + private readonly IEnumerable _externalSystems; + + public HealthController(IEnumerable externalSystems) + { + _externalSystems = externalSystems; + } + + [HttpGet("/healthz")] + public async Task Get() + { + try + { + foreach (var externalSystem in _externalSystems) + { + await externalSystem.Connect(); + } + } + catch (Exception) + { + return new BadRequestResult(); + } + + return Ok(); + } + } +} diff --git a/src/NetCoreKit.Infrastructure.AspNetCore.Miniservice/HomeController.cs b/src/NetCoreKit.Infrastructure.AspNetCore.Miniservice/HomeController.cs new file mode 100644 index 0000000..148e026 --- /dev/null +++ b/src/NetCoreKit.Infrastructure.AspNetCore.Miniservice/HomeController.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using NetCoreKit.Infrastructure.AspNetCore.Extensions; + +namespace NetCoreKit.Infrastructure.AspNetCore.Miniservice +{ + [Route("")] + [ApiVersionNeutral] + [ApiExplorerSettings(IgnoreApi = true)] + public class HomeController : Controller + { + private readonly string _basePath; + + public HomeController(IConfiguration config) + { + _basePath = config.GetBasePath() ?? "/"; + } + + [HttpGet] + public IActionResult Index() + { + return Redirect($"~{_basePath}swagger"); + } + } +} diff --git a/src/NetCoreKit.Infrastructure.AspNetCore.Miniservice/MiniServiceExtensions.cs b/src/NetCoreKit.Infrastructure.AspNetCore.Miniservice/MiniServiceExtensions.cs index f108e41..e527ea9 100644 --- a/src/NetCoreKit.Infrastructure.AspNetCore.Miniservice/MiniServiceExtensions.cs +++ b/src/NetCoreKit.Infrastructure.AspNetCore.Miniservice/MiniServiceExtensions.cs @@ -1,8 +1,13 @@ // Reference at https://thenewstack.io/miniservices-a-realistic-alternative-to-microservices +using System; +using System.Collections.Generic; +using System.Linq; using Microsoft.AspNetCore.Builder; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using NetCoreKit.Infrastructure.AspNetCore.Miniservice.ConfigureServices; namespace NetCoreKit.Infrastructure.AspNetCore.Miniservice { @@ -30,6 +35,21 @@ public static IServiceCollection AddMiniService(this IServiceCollect return services; } + public static IServiceCollection AddExternalSystemHealthChecks(this IServiceCollection services, + Func> extendExternalSystem = null) + { + var svcProvider = services.BuildServiceProvider(); + var config = svcProvider.GetService(); + if (!config.GetValue("SqlDatabase:Enabled")) + { + return services; + } + + if (extendExternalSystem == null) return services; + services.AddSingleton(p => extendExternalSystem(p).Append(p.GetService())); + return services; + } + public static IApplicationBuilder UseMiniService(this IApplicationBuilder app) { var appSvc = app.ApplicationServices; diff --git a/src/NetCoreKit.Infrastructure.EfCore/Extensions/WebHostExtensions.cs b/src/NetCoreKit.Infrastructure.EfCore/Extensions/WebHostExtensions.cs index 407fd76..ad6005d 100644 --- a/src/NetCoreKit.Infrastructure.EfCore/Extensions/WebHostExtensions.cs +++ b/src/NetCoreKit.Infrastructure.EfCore/Extensions/WebHostExtensions.cs @@ -74,14 +74,28 @@ public static IServiceProvider MigrateDbContext( var logger = serviceProvider.GetRequiredService>(); var context = serviceProvider.GetRequiredService(); - logger.LogInformation($"[VND] Migrating database associated with {typeof(TDbContext).FullName} context."); + logger.LogInformation($"[NCK] Migrating database associated with {typeof(TDbContext).FullName} context."); context.Database.OpenConnection(); if (!context.AllMigrationsApplied()) context.Database.Migrate(); - logger.LogInformation($"[VND] Start to seed data for {typeof(TDbContext).FullName} context."); + logger.LogInformation($"[NCK] Start to seed data for {typeof(TDbContext).FullName} context."); seeder(context, serviceProvider); - logger.LogInformation($"[VND] Migrated database associated with {typeof(TDbContext).FullName} context."); + logger.LogInformation($"[NCK] Migrated database associated with {typeof(TDbContext).FullName} context."); + return serviceProvider; + } + + public static IServiceProvider MigrateDbContext(this IServiceProvider serviceProvider) + { + var logger = serviceProvider.GetRequiredService>(); + var context = serviceProvider.GetRequiredService(); + + logger.LogInformation($"[NCK] Migrating database associated with {typeof(DbContext).FullName} context."); + context.Database.OpenConnection(); + if (!context.AllMigrationsApplied()) + context.Database.Migrate(); + + logger.LogInformation($"[NCK] Migrated database associated with {typeof(DbContext).FullName} context."); return serviceProvider; } } diff --git a/src/NetCoreKit.Infrastructure.EfCore/NetCoreKit.Infrastructure.EfCore.csproj b/src/NetCoreKit.Infrastructure.EfCore/NetCoreKit.Infrastructure.EfCore.csproj index e12c9e0..ae19f25 100644 --- a/src/NetCoreKit.Infrastructure.EfCore/NetCoreKit.Infrastructure.EfCore.csproj +++ b/src/NetCoreKit.Infrastructure.EfCore/NetCoreKit.Infrastructure.EfCore.csproj @@ -25,6 +25,7 @@ + diff --git a/src/NetCoreKit.Infrastructure/IExternalSystem.cs b/src/NetCoreKit.Infrastructure/IExternalSystem.cs new file mode 100644 index 0000000..a8c2d13 --- /dev/null +++ b/src/NetCoreKit.Infrastructure/IExternalSystem.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace NetCoreKit.Infrastructure +{ + public interface IExternalSystem + { + Task Connect(); + } +} diff --git a/src/NetCoreKit.Infrastructure/NetCoreKit.Infrastructure.csproj b/src/NetCoreKit.Infrastructure/NetCoreKit.Infrastructure.csproj new file mode 100644 index 0000000..3bd2608 --- /dev/null +++ b/src/NetCoreKit.Infrastructure/NetCoreKit.Infrastructure.csproj @@ -0,0 +1,20 @@ + + + + NetCoreKit.Infrastructure + 0.0.0 + netstandard2.0 + NetCoreKit.Infrastructure + NetCoreKit.Infrastructure + false + The infrastructure library for Cloud Native .NET Core Kit. + Supports base infrastructure in the library. + Thang Chung + git + https://github.com/cloudnative-netcore/netcore-kit.git + https://github.com/cloudnative-netcore/netcore-kit + https://github.com/cloudnative-netcore/netcore-kit/blob/master/LICENSE + Copyright (c) Thang Chung - 2018 + + +