diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..8688433 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,33 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/WebApi/bin/Debug/net6.0/WebApi.dll", + "args": [], + "cwd": "${workspaceFolder}/WebApi", + "stopAtEntry": false, + "serverReadyAction": { + "action": "openExternally", + "pattern": "\\bNow listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..756a09d --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/Clean-Architecture-WebApi.sln", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/Clean-Architecture-WebApi.sln", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/Clean-Architecture-WebApi.sln" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/Application/Features/Order/OrderCommands/OrderCommand.cs b/Application/Features/Order/OrderCommands/OrderCommand.cs new file mode 100644 index 0000000..2409b10 --- /dev/null +++ b/Application/Features/Order/OrderCommands/OrderCommand.cs @@ -0,0 +1,21 @@ +using MediatR; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Application.Features.Order.OrderCommands +{ + public class CreateOrder:IRequest + { + public string CustomerId { get; set; } + public string Description { get; set; } + public string Address { get; set; } + public ICollection ProductIds { get; set; } + } + public class CreateOrderResponse + { + public string OrderId { get; set; } + } +} diff --git a/Application/Features/Order/OrderCommands/OrderCommandHandlers.cs b/Application/Features/Order/OrderCommands/OrderCommandHandlers.cs new file mode 100644 index 0000000..56258e0 --- /dev/null +++ b/Application/Features/Order/OrderCommands/OrderCommandHandlers.cs @@ -0,0 +1,43 @@ +using Application.Features.Product.ProductCommands; +using AutoMapper; +using Domain.Interfaces; +using MediatR; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Application.Features.Order.OrderCommands +{ + public class OrderCommandHandlers : + IRequestHandler + { + private readonly IOrderRepository _orderRepository; + private readonly IProductRepository _productRepository; + private readonly IMapper _mapper; + + public OrderCommandHandlers(IOrderRepository orderRepo, IProductRepository productRepo, IMapper mapper) + { + _orderRepository = orderRepo; + _productRepository = productRepo; + _mapper = mapper; + } + + public async Task Handle(CreateOrder request, CancellationToken cancellationToken) + { + var products = await _productRepository.GetProductsByIds(request.ProductIds); + + var order = new Domain.Entities.Order + { + CustomerId = request.CustomerId, + Address = request.Address, + Description = request.Description, + Products = products + }; + await _orderRepository.AddAsync(order); + + return new CreateOrderResponse { OrderId = order.Id }; + } + } +} \ No newline at end of file diff --git a/Application/Features/Order/OrderQueries/OrderQueries.cs b/Application/Features/Order/OrderQueries/OrderQueries.cs new file mode 100644 index 0000000..c960c36 --- /dev/null +++ b/Application/Features/Order/OrderQueries/OrderQueries.cs @@ -0,0 +1,17 @@ +using MediatR; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Application.Features.Order.OrderQueries +{ + public class GetOrders:IRequest + { + } + public class GetOrdersResponse + { + public object Orders { get; set; } + } +} diff --git a/Application/Features/Order/OrderQueries/OrderQueriesHandlers.cs b/Application/Features/Order/OrderQueries/OrderQueriesHandlers.cs new file mode 100644 index 0000000..93831ea --- /dev/null +++ b/Application/Features/Order/OrderQueries/OrderQueriesHandlers.cs @@ -0,0 +1,32 @@ +using Application.Features.Order.OrderCommands; +using Application.Features.Product.ProductCommands; +using AutoMapper; +using Domain.Interfaces; +using MediatR; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Application.Features.Order.OrderQueries +{ + public class OrderQueriesHandlers : + IRequestHandler + { + private readonly IOrderRepository _orderRepository; + private readonly IMapper _mapper; + + public OrderQueriesHandlers(IOrderRepository orderRepo, IMapper mapper) + { + _orderRepository = orderRepo; + _mapper = mapper; + } + + public async Task Handle(GetOrders request, CancellationToken cancellationToken) + { + var orders = _orderRepository.GetAll(); + return new() { Orders = orders }; + } + } +} diff --git a/Application/ServiceRegistration.cs b/Application/ServiceRegistration.cs index 5603e05..1053b2f 100644 --- a/Application/ServiceRegistration.cs +++ b/Application/ServiceRegistration.cs @@ -1,4 +1,5 @@ using Application.Features.Category.CategoryQueries; +using Application.Features.Order.OrderCommands; using Application.Features.Product.ProductCommands; using AutoMapper; using Domain.Entities; @@ -20,10 +21,18 @@ public class RegisterMapper : Profile { public RegisterMapper() { + // Product Mapeprs CreateMap(); CreateMap(); + // Auth Mappers CreateMap(); + // Category Mappers CreateMap(); + // Order Mappers + //CreateMap() + // .ForMember(dest => dest.Products, opt => opt.Ignore()) + // .ForSourceMember(src => src.ProductIds, opt => opt.DoNotValidate()); + } } public static void AddMapperServices(this IServiceCollection services) diff --git a/Clean-Architecture-WebApi.sln b/Clean-Architecture-WebApi.sln index 924f782..97dfaf8 100644 --- a/Clean-Architecture-WebApi.sln +++ b/Clean-Architecture-WebApi.sln @@ -1,15 +1,17 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.5.33627.172 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.34114.132 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApi", "WebApi\WebApi.csproj", "{C1BBCF62-2B34-4087-A393-CD006853D970}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApi", "WebApi\WebApi.csproj", "{C1BBCF62-2B34-4087-A393-CD006853D970}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure", "Infrastructure\Infrastructure.csproj", "{2621E73D-E51C-4E4E-80E5-CB2DD402C01E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Infrastructure", "Infrastructure\Infrastructure.csproj", "{2621E73D-E51C-4E4E-80E5-CB2DD402C01E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Domain", "Domain\Domain.csproj", "{9AF4F777-450C-4E3A-8F88-5E1D79DC9D9D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Domain", "Domain\Domain.csproj", "{9AF4F777-450C-4E3A-8F88-5E1D79DC9D9D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Application", "Application\Application.csproj", "{9472D01F-C7B3-4A9D-B6E4-3A650BA4D5F9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Application", "Application\Application.csproj", "{9472D01F-C7B3-4A9D-B6E4-3A650BA4D5F9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApi.Tests", "WebApi.Tests\WebApi.Tests.csproj", "{A5AE6E8E-ABC9-4B29-A3A3-0A03A91A0B4E}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -33,6 +35,10 @@ Global {9472D01F-C7B3-4A9D-B6E4-3A650BA4D5F9}.Debug|Any CPU.Build.0 = Debug|Any CPU {9472D01F-C7B3-4A9D-B6E4-3A650BA4D5F9}.Release|Any CPU.ActiveCfg = Release|Any CPU {9472D01F-C7B3-4A9D-B6E4-3A650BA4D5F9}.Release|Any CPU.Build.0 = Release|Any CPU + {A5AE6E8E-ABC9-4B29-A3A3-0A03A91A0B4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A5AE6E8E-ABC9-4B29-A3A3-0A03A91A0B4E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A5AE6E8E-ABC9-4B29-A3A3-0A03A91A0B4E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A5AE6E8E-ABC9-4B29-A3A3-0A03A91A0B4E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Domain/Entities/Order.cs b/Domain/Entities/Order.cs new file mode 100644 index 0000000..7f36b19 --- /dev/null +++ b/Domain/Entities/Order.cs @@ -0,0 +1,17 @@ +using Domain.Entities.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Domain.Entities +{ + public class Order:BaseEntity + { + public string CustomerId { get; set; } + public string Description { get; set; } + public string Address { get; set; } + public ICollection Products { get; set; } + } +} diff --git a/Domain/Entities/Product.cs b/Domain/Entities/Product.cs index c4c227a..54490b3 100644 --- a/Domain/Entities/Product.cs +++ b/Domain/Entities/Product.cs @@ -14,5 +14,6 @@ public class Product:BaseEntity public bool IsActive { get; set; } public int Stock { get; set; } public string CategoryId { get; set; } + public ICollection Orders { get; set; } } } diff --git a/Domain/Interfaces/IOrderRepository.cs b/Domain/Interfaces/IOrderRepository.cs new file mode 100644 index 0000000..2e4c867 --- /dev/null +++ b/Domain/Interfaces/IOrderRepository.cs @@ -0,0 +1,14 @@ +using Domain.Entities; +using Domain.Interfaces.Generic; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Domain.Interfaces +{ + public interface IOrderRepository:IGenericRepository + { + } +} diff --git a/Domain/Interfaces/IProductRepository.cs b/Domain/Interfaces/IProductRepository.cs index 087f219..8f5f091 100644 --- a/Domain/Interfaces/IProductRepository.cs +++ b/Domain/Interfaces/IProductRepository.cs @@ -11,5 +11,6 @@ namespace Domain.Interfaces public interface IProductRepository : IGenericRepository { Task> GetProductsByCategoryId(string CategoryId); + Task> GetProductsByIds(ICollection productIds); } } diff --git a/Infrastructure/Contexts/Context.cs b/Infrastructure/Contexts/Context.cs index 93280d5..cab21f5 100644 --- a/Infrastructure/Contexts/Context.cs +++ b/Infrastructure/Contexts/Context.cs @@ -16,6 +16,7 @@ public class EFDBContext : IdentityDbContext Products { get; set; } public DbSet Categories { get; set; } + public DbSet Order { get; set; } public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) { diff --git a/Infrastructure/Migrations/20231001184322_added_order_tables.Designer.cs b/Infrastructure/Migrations/20231001184322_added_order_tables.Designer.cs new file mode 100644 index 0000000..263a843 --- /dev/null +++ b/Infrastructure/Migrations/20231001184322_added_order_tables.Designer.cs @@ -0,0 +1,381 @@ +// +using System; +using Infrastructure.Contexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Infrastructure.Migrations +{ + [DbContext(typeof(EFDBContext))] + [Migration("20231001184322_added_order_tables")] + partial class added_order_tables + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.21"); + + modelBuilder.Entity("Domain.Entities.Category", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Domain.Entities.Identity.ApplicationRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Domain.Entities.Identity.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("Surname") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserNumber") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Domain.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Address") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CustomerId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Order"); + }); + + modelBuilder.Entity("Domain.Entities.Product", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("REAL"); + + b.Property("Stock") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("OrderProduct", b => + { + b.Property("OrdersId") + .HasColumnType("TEXT"); + + b.Property("ProductsId") + .HasColumnType("TEXT"); + + b.HasKey("OrdersId", "ProductsId"); + + b.HasIndex("ProductsId"); + + b.ToTable("OrderProduct"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Domain.Entities.Identity.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Domain.Entities.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Domain.Entities.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Domain.Entities.Identity.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Domain.Entities.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("OrderProduct", b => + { + b.HasOne("Domain.Entities.Order", null) + .WithMany() + .HasForeignKey("OrdersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Product", null) + .WithMany() + .HasForeignKey("ProductsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Infrastructure/Migrations/20231001184322_added_order_tables.cs b/Infrastructure/Migrations/20231001184322_added_order_tables.cs new file mode 100644 index 0000000..f3582f8 --- /dev/null +++ b/Infrastructure/Migrations/20231001184322_added_order_tables.cs @@ -0,0 +1,66 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Migrations +{ + public partial class added_order_tables : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Order", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + CustomerId = table.Column(type: "TEXT", nullable: false), + Description = table.Column(type: "TEXT", nullable: false), + Address = table.Column(type: "TEXT", nullable: false), + Date = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Order", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "OrderProduct", + columns: table => new + { + OrdersId = table.Column(type: "TEXT", nullable: false), + ProductsId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrderProduct", x => new { x.OrdersId, x.ProductsId }); + table.ForeignKey( + name: "FK_OrderProduct_Order_OrdersId", + column: x => x.OrdersId, + principalTable: "Order", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_OrderProduct_Products_ProductsId", + column: x => x.ProductsId, + principalTable: "Products", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_OrderProduct_ProductsId", + table: "OrderProduct", + column: "ProductsId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OrderProduct"); + + migrationBuilder.DropTable( + name: "Order"); + } + } +} diff --git a/Infrastructure/Migrations/EFDBContextModelSnapshot.cs b/Infrastructure/Migrations/EFDBContextModelSnapshot.cs index b9eb657..e42aa40 100644 --- a/Infrastructure/Migrations/EFDBContextModelSnapshot.cs +++ b/Infrastructure/Migrations/EFDBContextModelSnapshot.cs @@ -15,7 +15,7 @@ partial class EFDBContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "7.0.11"); + modelBuilder.HasAnnotation("ProductVersion", "6.0.21"); modelBuilder.Entity("Domain.Entities.Category", b => { @@ -136,6 +136,31 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AspNetUsers", (string)null); }); + modelBuilder.Entity("Domain.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Address") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CustomerId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Order"); + }); + modelBuilder.Entity("Domain.Entities.Product", b => { b.Property("Id") @@ -268,6 +293,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AspNetUserTokens", (string)null); }); + modelBuilder.Entity("OrderProduct", b => + { + b.Property("OrdersId") + .HasColumnType("TEXT"); + + b.Property("ProductsId") + .HasColumnType("TEXT"); + + b.HasKey("OrdersId", "ProductsId"); + + b.HasIndex("ProductsId"); + + b.ToTable("OrderProduct"); + }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { b.HasOne("Domain.Entities.Identity.ApplicationRole", null) @@ -318,6 +358,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); + + modelBuilder.Entity("OrderProduct", b => + { + b.HasOne("Domain.Entities.Order", null) + .WithMany() + .HasForeignKey("OrdersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Product", null) + .WithMany() + .HasForeignKey("ProductsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); #pragma warning restore 612, 618 } } diff --git a/Infrastructure/Repositories/OrderRepository.cs b/Infrastructure/Repositories/OrderRepository.cs new file mode 100644 index 0000000..956ffcd --- /dev/null +++ b/Infrastructure/Repositories/OrderRepository.cs @@ -0,0 +1,19 @@ +using Domain.Entities; +using Domain.Interfaces; +using Infrastructure.Contexts; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Infrastructure.Repositories +{ + public class OrderRepository:GenericRepository,IOrderRepository + { + public OrderRepository(EFDBContext context) : base(context) + { + + } + } +} diff --git a/Infrastructure/Repositories/ProductRepository.cs b/Infrastructure/Repositories/ProductRepository.cs index 2b48fe3..492a589 100644 --- a/Infrastructure/Repositories/ProductRepository.cs +++ b/Infrastructure/Repositories/ProductRepository.cs @@ -19,10 +19,9 @@ public ProductRepository(EFDBContext context) : base(context) db = context; } - public async Task> GetProductsByCategoryId(string CategoryId) - { - var products = await db.Products.Where(i => i.CategoryId == CategoryId).ToListAsync(); - return products; - } + public async Task> GetProductsByCategoryId(string CategoryId) => await db.Products.Where(i => i.CategoryId == CategoryId).ToListAsync(); + + + public async Task> GetProductsByIds(ICollection productIds) => await db.Products.Where(p => productIds.Contains(p.Id)).ToListAsync(); } } diff --git a/Infrastructure/ServiceRegistration.cs b/Infrastructure/ServiceRegistration.cs index 0338262..55c4c98 100644 --- a/Infrastructure/ServiceRegistration.cs +++ b/Infrastructure/ServiceRegistration.cs @@ -32,6 +32,7 @@ public static void AddInfrastructureServices(this IServiceCollection services) services.AddDbContext(options => options.UseSqlite(Configuration.ConnectionString)); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); } diff --git a/README.md b/README.md index 48e7883..5d82a38 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ - Identity - AspNetCoreRateLimit using - Serilog using +- SignalR +- SignalR Notification ## Packages Used @@ -23,6 +25,7 @@ - Serilog - Serilog.Sinks.File - Microsoft.AspNetCore.Mvc.Version +- Microsoft.AspNetCore.SignalR ## Getting Started diff --git a/WebApi.Tests/Tests/ProductTest.cs b/WebApi.Tests/Tests/ProductTest.cs new file mode 100644 index 0000000..5975d43 --- /dev/null +++ b/WebApi.Tests/Tests/ProductTest.cs @@ -0,0 +1,39 @@ +using Domain.Entities; +using System.Net.Http; +using System.Text.Json; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Testing; +using Xunit; +using Infrastructure.Contexts; +using Moq; +using Application.Features; +using Infrastructure.Repositories; +using Domain.Interfaces; + +namespace WebApi.Tests +{ + public class ProductTests + { + public interface IDbContext + { + public IList Products { get; } + } + internal class DbContext : IDbContext + { + public IList Products { get; } = new List(); + } + + //[Fact] + //public async Task GetAllProductsTest() + //{ + // var handler = new AllProductQueriesHandlers(IProductRepository); + // var result = await handler.Handle(new GetProducts(), CancellationToken.None); + // result.(); + // result.TodoLists.ShouldBeOfType>(); + // result.TodoLists.Count.ShouldBe(2); + //} + } + +} + diff --git a/WebApi.Tests/Usings.cs b/WebApi.Tests/Usings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/WebApi.Tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/WebApi.Tests/WebApi.Tests.csproj b/WebApi.Tests/WebApi.Tests.csproj new file mode 100644 index 0000000..41ae027 --- /dev/null +++ b/WebApi.Tests/WebApi.Tests.csproj @@ -0,0 +1,32 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/WebApi/Controllers/AccountController.cs b/WebApi/Controllers/AccountController.cs index 9579bdd..031a3d2 100644 --- a/WebApi/Controllers/AccountController.cs +++ b/WebApi/Controllers/AccountController.cs @@ -30,7 +30,7 @@ public async Task Login(LoginCommand command) } [HttpGet("GetAllUsers")] - public async Task GetMUserById([FromQuery] GetAllUsers query) + public async Task GetAllUsers([FromQuery] GetAllUsers query) { GetAllUsersResponse response = await _mediator.Send(query); return Ok(response); @@ -38,7 +38,7 @@ public async Task GetMUserById([FromQuery] GetAllUsers query) [HttpGet("GetUserById")] - public async Task GetMUserById([FromQuery] GetUserById query) + public async Task GetUserById([FromQuery] GetUserById query) { GetUserByIdResponse response = await _mediator.Send(query); return Ok(response); diff --git a/WebApi/Controllers/OrderController.cs b/WebApi/Controllers/OrderController.cs new file mode 100644 index 0000000..bfdda99 --- /dev/null +++ b/WebApi/Controllers/OrderController.cs @@ -0,0 +1,38 @@ +using Application.Features.Order.OrderQueries; +using Application.Features; +using MediatR; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Application.Features.Order.OrderCommands; +using System.Net; + +namespace WebApi.Controllers +{ + [ApiController] + [ApiVersion("2.0")] + [Route("api/[controller]")] + public class OrderController : ControllerBase + { + private readonly IMediator _mediator; + + public OrderController(IMediator mediator) + { + _mediator = mediator; + } + + + [HttpGet("GetOrders")] + public async Task GetOrders([FromQuery] GetOrders query) + { + GetOrdersResponse response = await _mediator.Send(query); + return StatusCode((int)HttpStatusCode.Created); + } + + [HttpPost("CreateOrder")] + public async Task CreateOrder([FromQuery] CreateOrder command) + { + CreateOrderResponse response = await _mediator.Send(command); + return Ok(response); + } + } +} diff --git a/WebApi/Controllers/ProductController.cs b/WebApi/Controllers/ProductController.cs index eb7f028..fc61673 100644 --- a/WebApi/Controllers/ProductController.cs +++ b/WebApi/Controllers/ProductController.cs @@ -2,6 +2,8 @@ using Application.Features.Product.ProductCommands; using MediatR; using Microsoft.AspNetCore.Mvc; +using System.Net; +using WebApi.SignalR.HubServices; namespace WebApi.Controllers { @@ -11,10 +13,11 @@ namespace WebApi.Controllers public class ProductController : ControllerBase { private readonly IMediator _mediator; - - public ProductController(IMediator mediator) + readonly IProductHubService _productHubService; + public ProductController(IMediator mediator, IProductHubService productHubService) { _mediator = mediator; + _productHubService = productHubService; } @@ -43,18 +46,28 @@ public async Task GetProductById([FromQuery] GetProductById query public async Task CreateProduct([FromBody] CreateProduct query) { CreateProductResponse response = await _mediator.Send(query); - return Ok(response); + + await _productHubService.ProductAddedMessageAsync($"Product named {query.Name} has been added."); + + return StatusCode((int)HttpStatusCode.Created); } + [HttpPut("UpdateProduct")] public async Task UpdateProduct([FromBody] UpdateProduct query) { UpdateProductResponse response = await _mediator.Send(query); + + await _productHubService.ProductAddedMessageAsync($"{query.Name} isminde ürün eklenmiştir."); + + await _productHubService.ProductAddedMessageAsync($"Product named {query.Name} has been updated."); + return Ok(response); } [HttpDelete("RemoveProduct")] public async Task RemoveProduct([FromQuery] DeleteCommand query) { DeleteCommandResponse response = await _mediator.Send(query); + await _productHubService.ProductAddedMessageAsync($"Product with id {query.Id} removed."); return Ok(response); } } diff --git a/WebApi/Database/webapidatabase.db b/WebApi/Database/webapidatabase.db index 1151e77..e4a5172 100644 Binary files a/WebApi/Database/webapidatabase.db and b/WebApi/Database/webapidatabase.db differ diff --git a/WebApi/Database/webapidatabase.db-shm b/WebApi/Database/webapidatabase.db-shm index fbdb28a..ba66b59 100644 Binary files a/WebApi/Database/webapidatabase.db-shm and b/WebApi/Database/webapidatabase.db-shm differ diff --git a/WebApi/Database/webapidatabase.db-wal b/WebApi/Database/webapidatabase.db-wal index 0f113e3..174ddc8 100644 Binary files a/WebApi/Database/webapidatabase.db-wal and b/WebApi/Database/webapidatabase.db-wal differ diff --git a/WebApi/ErrorHandlingMiddleware.cs b/WebApi/ErrorHandlingMiddleware.cs index 44eeab3..1b18e8f 100644 --- a/WebApi/ErrorHandlingMiddleware.cs +++ b/WebApi/ErrorHandlingMiddleware.cs @@ -24,11 +24,11 @@ public async Task Invoke(HttpContext context) } catch (Exception ex) { - // Hata yakalandığında loglama işlemi + // Logs Log.Error("Error", ex, ex.Message); Log.Error("----------------------------"); - // Hata mesajını response body'ye yazma + // Errors var response = context.Response; response.ContentType = "text/plain"; response.StatusCode = StatusCodes.Status500InternalServerError; diff --git a/WebApi/Program.cs b/WebApi/Program.cs index 4c03a3a..6628452 100644 --- a/WebApi/Program.cs +++ b/WebApi/Program.cs @@ -5,22 +5,23 @@ using Microsoft.OpenApi.Models; using Serilog; using WebApi; +using WebApi.SignalR; var builder = WebApplication.CreateBuilder(args); builder.Services.AddInfrastructureServices(); builder.Services.AddApplicationServices(); builder.Services.AddMapperServices(); +builder.Services.AddSignalRServices(); var appDataFolder = Path.Combine(builder.Environment.ContentRootPath, "Database"); AppDomain.CurrentDomain.SetData("DataDirectory", appDataFolder); - - builder.Services.AddCors(options => options.AddDefaultPolicy(policy => policy.WithOrigins("*").AllowAnyHeader().AllowAnyMethod() )); +// Version builder.Services.AddSwaggerGen(c => { c.SwaggerDoc("v2", new OpenApiInfo { Title = "Clean-Architecture-WebApi", Version = "v2" }); @@ -43,6 +44,13 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); +// MiniProfiler Settings +builder.Services.AddMiniProfiler(options => +{ + options.RouteBasePath = "/profiler"; +}).AddEntityFramework(); + + // Serilog Using Log.Logger = new LoggerConfiguration() .MinimumLevel.Error() @@ -62,7 +70,7 @@ if (app.Environment.IsDevelopment()) { app.UseSwagger(); - // version 2.0 start configuration + // Version 2.0 start configuration app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v2/swagger.json", "Clean-Architecture-WebApi v2"); @@ -75,8 +83,15 @@ app.UseStaticFiles(); +app.UseCors(); + + +app.UseMiniProfiler(); + app.UseAuthorization(); app.MapControllers(); +app.MapHubs(); + app.Run(); diff --git a/WebApi/SignalR/HubRegistration.cs b/WebApi/SignalR/HubRegistration.cs new file mode 100644 index 0000000..a90ff3b --- /dev/null +++ b/WebApi/SignalR/HubRegistration.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Builder; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using WebApi.SignalR.Hubs; + +namespace WebApi.SignalR +{ + public static class HubRegistration + { + public static void MapHubs(this WebApplication webApplication) + { + webApplication.MapHub("/products-hub"); + } + } +} diff --git a/WebApi/SignalR/HubServices/ProductHubService.cs b/WebApi/SignalR/HubServices/ProductHubService.cs new file mode 100644 index 0000000..31d351e --- /dev/null +++ b/WebApi/SignalR/HubServices/ProductHubService.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.SignalR; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using WebApi.SignalR.Hubs; + +namespace WebApi.SignalR.HubServices +{ + public class ProductHubService:IProductHubService + { + readonly IHubContext _hubContext; + + public ProductHubService(IHubContext hubContext) + { + _hubContext = hubContext; + } + + public async Task ProductAddedMessageAsync(string message) + { + await _hubContext.Clients.All.SendAsync(ReceiveFunctionNames.ProductAddedMessage, message); + } + public async Task ProductUpdatedMessageAsync(string message) + { + await _hubContext.Clients.All.SendAsync(ReceiveFunctionNames.ProductAddedMessage, message); + } + public async Task ProductRemovedMessageAsync(string message) + { + await _hubContext.Clients.All.SendAsync(ReceiveFunctionNames.ProductAddedMessage, message); + } + } +} diff --git a/WebApi/SignalR/Hubs/ProductHub.cs b/WebApi/SignalR/Hubs/ProductHub.cs new file mode 100644 index 0000000..a7b4aa1 --- /dev/null +++ b/WebApi/SignalR/Hubs/ProductHub.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.SignalR; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace WebApi.SignalR.Hubs +{ + public class ProductHub : Hub + { + } +} diff --git a/WebApi/SignalR/IHubServices/IProductHubService.cs b/WebApi/SignalR/IHubServices/IProductHubService.cs new file mode 100644 index 0000000..09672af --- /dev/null +++ b/WebApi/SignalR/IHubServices/IProductHubService.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace WebApi.SignalR.HubServices +{ + public interface IProductHubService + { + Task ProductAddedMessageAsync(string message); + Task ProductUpdatedMessageAsync(string message); + Task ProductRemovedMessageAsync(string message); + } +} diff --git a/WebApi/SignalR/ReceiveFunctionNames.cs b/WebApi/SignalR/ReceiveFunctionNames.cs new file mode 100644 index 0000000..d82c5b9 --- /dev/null +++ b/WebApi/SignalR/ReceiveFunctionNames.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace WebApi.SignalR +{ + public static class ReceiveFunctionNames + { + public const string ProductAddedMessage = "receiveProductAddedMessage"; + } +} diff --git a/WebApi/SignalR/ServiceRegistration.cs b/WebApi/SignalR/ServiceRegistration.cs new file mode 100644 index 0000000..4b861ce --- /dev/null +++ b/WebApi/SignalR/ServiceRegistration.cs @@ -0,0 +1,26 @@ +using Domain.Entities.Identity; +using Domain.Interfaces; +using Infrastructure.Contexts; +using Infrastructure.Repositories; +using Infrastructure.Services; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using WebApi.SignalR.HubServices; + +namespace Infrastructure +{ + public static class ServiceRegistration + { + public static void AddSignalRServices(this IServiceCollection collection) + { + collection.AddTransient(); + collection.AddSignalR(); + } + } +} \ No newline at end of file diff --git a/WebApi/WebApi.csproj b/WebApi/WebApi.csproj index de10420..c095d66 100644 --- a/WebApi/WebApi.csproj +++ b/WebApi/WebApi.csproj @@ -9,10 +9,13 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/WebApi/logs.txt b/WebApi/logs.txt index ec410a0..08bf1f8 100644 --- a/WebApi/logs.txt +++ b/WebApi/logs.txt @@ -10,3 +10,11 @@ 2023-09-29 19:29:46.844 +03:00 [ERR] ---------------------------- 2023-09-29 19:45:23.872 +03:00 [ERR] Error 2023-09-29 19:45:23.895 +03:00 [ERR] ---------------------------- +2023-10-30 16:18:50.114 +03:00 [ERR] Error +2023-10-30 16:18:50.119 +03:00 [ERR] ---------------------------- +2023-10-30 16:19:51.360 +03:00 [ERR] Error +2023-10-30 16:19:51.360 +03:00 [ERR] ---------------------------- +2023-10-30 16:28:57.009 +03:00 [ERR] Error +2023-10-30 16:28:57.010 +03:00 [ERR] ---------------------------- +2023-10-30 16:37:09.549 +03:00 [ERR] Error +2023-10-30 16:37:09.549 +03:00 [ERR] ----------------------------