diff --git a/Models/TodoItemDTO.cs b/BLL/DTO/TodoItemDTO.cs similarity index 74% rename from Models/TodoItemDTO.cs rename to BLL/DTO/TodoItemDTO.cs index e66a500a..6276620e 100644 --- a/Models/TodoItemDTO.cs +++ b/BLL/DTO/TodoItemDTO.cs @@ -1,7 +1,7 @@ -namespace TodoApi.Models +namespace TodoApiDTO.BLL.DTO { #region snippet - public class TodoItemDTO + public class TodoItemDto { public long Id { get; set; } public string Name { get; set; } diff --git a/BLL/Interfaces/ITodoItemService.cs b/BLL/Interfaces/ITodoItemService.cs new file mode 100644 index 00000000..2819afb7 --- /dev/null +++ b/BLL/Interfaces/ITodoItemService.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using TodoApiDTO.BLL.DTO; + +namespace TodoApiDTO.BLL.Interfaces +{ + public interface ITodoItemService + { + Task> GetTodoItems(); + Task GetTodoItem(long id); + Task UpdateTodoItem(long id, TodoItemDto todoItemDto); + Task CreateTodoItem(TodoItemDto todoItemDto); + Task DeleteTodoItem(long id); + } +} \ No newline at end of file diff --git a/BLL/Services/TodoItemService.cs b/BLL/Services/TodoItemService.cs new file mode 100644 index 00000000..fe414ad6 --- /dev/null +++ b/BLL/Services/TodoItemService.cs @@ -0,0 +1,111 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using TodoApiDTO.BLL.DTO; +using TodoApiDTO.BLL.Interfaces; +using TodoApiDTO.DAL.Models; + +namespace TodoApiDTO.BLL.Services +{ + public class TodoItemService : ITodoItemService + { + private readonly TodoContext _context; + + public TodoItemService(TodoContext context) + { + _context = context; + } + + public async Task> GetTodoItems() + { + return await _context.TodoItems + .Select(x => ItemToDto(x)) + .ToListAsync(); + } + + public async Task GetTodoItem(long id) + { + var todoItem = await _context.TodoItems.FindAsync(id); + + if (todoItem == null) + { + return null; + } + + return ItemToDto(todoItem); + } + + public async Task UpdateTodoItem(long id, TodoItemDto todoItemDto) + { + if (id != todoItemDto.Id) + { + return false; + } + + var todoItem = await _context.TodoItems.FindAsync(id); + if (todoItem == null) + { + return false; + } + + todoItem.Name = todoItemDto.Name; + todoItem.IsComplete = todoItemDto.IsComplete; + + try + { + await _context.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + if (!TodoItemExists(id)) + { + return false; + } + throw; + } + + return true; + } + + public async Task CreateTodoItem(TodoItemDto todoItemDto) + { + var todoItem = new TodoItem + { + IsComplete = todoItemDto.IsComplete, + Name = todoItemDto.Name + }; + + _context.TodoItems.Add(todoItem); + await _context.SaveChangesAsync(); + + return ItemToDto(todoItem); + } + + public async Task DeleteTodoItem(long id) + { + var todoItem = await _context.TodoItems.FindAsync(id); + + if (todoItem == null) + { + return false; + } + + _context.TodoItems.Remove(todoItem); + await _context.SaveChangesAsync(); + + return true; + } + + private bool TodoItemExists(long id) => + _context.TodoItems.Any(e => e.Id == id); + + private static TodoItemDto ItemToDto(TodoItem todoItem) => + new TodoItemDto + { + Id = todoItem.Id, + Name = todoItem.Name, + IsComplete = todoItem.IsComplete + }; + } +} \ No newline at end of file diff --git a/Controllers/TodoItemsController.cs b/Controllers/TodoItemsController.cs index 0ef138e7..044e20f4 100644 --- a/Controllers/TodoItemsController.cs +++ b/Controllers/TodoItemsController.cs @@ -1,66 +1,48 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; -using TodoApi.Models; +using Microsoft.AspNetCore.Mvc; +using TodoApiDTO.BLL.DTO; +using TodoApiDTO.BLL.Interfaces; -namespace TodoApi.Controllers +namespace TodoApiDTO.Controllers { [Route("api/[controller]")] [ApiController] public class TodoItemsController : ControllerBase { - private readonly TodoContext _context; + private readonly ITodoItemService _todoItemService; - public TodoItemsController(TodoContext context) + public TodoItemsController(ITodoItemService todoItemService) { - _context = context; + _todoItemService = todoItemService; } [HttpGet] - public async Task>> GetTodoItems() + public async Task>> GetTodoItems() { - return await _context.TodoItems - .Select(x => ItemToDTO(x)) - .ToListAsync(); + var todoItems = await _todoItemService.GetTodoItems(); + return Ok(todoItems); } [HttpGet("{id}")] - public async Task> GetTodoItem(long id) + public async Task> GetTodoItem(long id) { - var todoItem = await _context.TodoItems.FindAsync(id); + var todoItem = await _todoItemService.GetTodoItem(id); if (todoItem == null) { return NotFound(); } - return ItemToDTO(todoItem); + return Ok(todoItem); } [HttpPut("{id}")] - public async Task UpdateTodoItem(long id, TodoItemDTO todoItemDTO) + public async Task UpdateTodoItem(long id, TodoItemDto todoItemDto) { - if (id != todoItemDTO.Id) - { - return BadRequest(); - } + var result = await _todoItemService.UpdateTodoItem(id, todoItemDto); - var todoItem = await _context.TodoItems.FindAsync(id); - if (todoItem == null) - { - return NotFound(); - } - - todoItem.Name = todoItemDTO.Name; - todoItem.IsComplete = todoItemDTO.IsComplete; - - try - { - await _context.SaveChangesAsync(); - } - catch (DbUpdateConcurrencyException) when (!TodoItemExists(id)) + if (!result) { return NotFound(); } @@ -69,48 +51,27 @@ public async Task UpdateTodoItem(long id, TodoItemDTO todoItemDTO } [HttpPost] - public async Task> CreateTodoItem(TodoItemDTO todoItemDTO) + public async Task> CreateTodoItem(TodoItemDto todoItemDto) { - var todoItem = new TodoItem - { - IsComplete = todoItemDTO.IsComplete, - Name = todoItemDTO.Name - }; - - _context.TodoItems.Add(todoItem); - await _context.SaveChangesAsync(); + var createdTodoItem = await _todoItemService.CreateTodoItem(todoItemDto); return CreatedAtAction( nameof(GetTodoItem), - new { id = todoItem.Id }, - ItemToDTO(todoItem)); + new { id = createdTodoItem.Id }, + createdTodoItem); } [HttpDelete("{id}")] public async Task DeleteTodoItem(long id) { - var todoItem = await _context.TodoItems.FindAsync(id); + var result = await _todoItemService.DeleteTodoItem(id); - if (todoItem == null) + if (!result) { return NotFound(); } - _context.TodoItems.Remove(todoItem); - await _context.SaveChangesAsync(); - return NoContent(); } - - private bool TodoItemExists(long id) => - _context.TodoItems.Any(e => e.Id == id); - - private static TodoItemDTO ItemToDTO(TodoItem todoItem) => - new TodoItemDTO - { - Id = todoItem.Id, - Name = todoItem.Name, - IsComplete = todoItem.IsComplete - }; } } diff --git a/Models/TodoContext.cs b/DAL/Models/TodoContext.cs similarity index 89% rename from Models/TodoContext.cs rename to DAL/Models/TodoContext.cs index 6e59e363..4237351f 100644 --- a/Models/TodoContext.cs +++ b/DAL/Models/TodoContext.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore; -namespace TodoApi.Models +namespace TodoApiDTO.DAL.Models { public class TodoContext : DbContext { diff --git a/Models/TodoItem.cs b/DAL/Models/TodoItem.cs similarity index 87% rename from Models/TodoItem.cs rename to DAL/Models/TodoItem.cs index 1f6e5465..4ecd9745 100644 --- a/Models/TodoItem.cs +++ b/DAL/Models/TodoItem.cs @@ -1,4 +1,4 @@ -namespace TodoApi.Models +namespace TodoApiDTO.DAL.Models { #region snippet public class TodoItem diff --git a/Migrations/20230522140042_InitialMigration.Designer.cs b/Migrations/20230522140042_InitialMigration.Designer.cs new file mode 100644 index 00000000..c1fc6f03 --- /dev/null +++ b/Migrations/20230522140042_InitialMigration.Designer.cs @@ -0,0 +1,46 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TodoApiDTO.DAL.Models; + +namespace TodoApiDTO.Migrations +{ + [DbContext(typeof(TodoContext))] + [Migration("20230522140042_InitialMigration")] + partial class InitialMigration + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("TodoApi.Models.TodoItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("IsComplete") + .HasColumnType("bit"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Secret") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("TodoItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20230522140042_InitialMigration.cs b/Migrations/20230522140042_InitialMigration.cs new file mode 100644 index 00000000..f908b4f1 --- /dev/null +++ b/Migrations/20230522140042_InitialMigration.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace TodoApiDTO.Migrations +{ + public partial class InitialMigration : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "TodoItems", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(nullable: true), + IsComplete = table.Column(nullable: false), + Secret = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_TodoItems", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TodoItems"); + } + } +} diff --git a/Migrations/TodoContextModelSnapshot.cs b/Migrations/TodoContextModelSnapshot.cs new file mode 100644 index 00000000..9d815851 --- /dev/null +++ b/Migrations/TodoContextModelSnapshot.cs @@ -0,0 +1,44 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TodoApiDTO.DAL.Models; + +namespace TodoApiDTO.Migrations +{ + [DbContext(typeof(TodoContext))] + partial class TodoContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("TodoApi.Models.TodoItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("IsComplete") + .HasColumnType("bit"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Secret") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("TodoItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Program.cs b/Program.cs index b27ac16a..a8db26be 100644 --- a/Program.cs +++ b/Program.cs @@ -1,13 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; +using Serilog; -namespace TodoApi +namespace TodoApiDTO { public class Program { @@ -21,6 +16,10 @@ public static IHostBuilder CreateHostBuilder(string[] args) => .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); + }) + .UseSerilog((hostingContext, loggerConfiguration) => + { + loggerConfiguration.ReadFrom.Configuration(hostingContext.Configuration); }); } } diff --git a/Startup.cs b/Startup.cs index bbfbc83d..a80a73c0 100644 --- a/Startup.cs +++ b/Startup.cs @@ -1,19 +1,15 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.HttpsPolicy; -using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using TodoApi.Models; +using Microsoft.OpenApi.Models; +using TodoApiDTO.BLL.Interfaces; +using TodoApiDTO.BLL.Services; +using TodoApiDTO.DAL.Models; -namespace TodoApi +namespace TodoApiDTO { public class Startup { @@ -27,9 +23,19 @@ public Startup(IConfiguration configuration) // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { + services.AddMvc(); + + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo { Title = "Todo API", Version = "v1" }); + }); + services.AddDbContext(opt => - opt.UseInMemoryDatabase("TodoList")); + opt.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); + services.AddControllers(); + + services.AddScoped(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -50,6 +56,13 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { endpoints.MapControllers(); }); + + app.UseSwagger(); + + app.UseSwaggerUI(c => + { + c.SwaggerEndpoint("v1/swagger.json", "Todo API V1"); + }); } } } diff --git a/TodoApiDTO.csproj b/TodoApi.csproj similarity index 63% rename from TodoApiDTO.csproj rename to TodoApi.csproj index bba6f6af..db258e58 100644 --- a/TodoApiDTO.csproj +++ b/TodoApi.csproj @@ -2,6 +2,7 @@ netcoreapp3.1 + TodoApiDTO @@ -12,6 +13,14 @@ + + + + + + + + diff --git a/TodoApiDTO.sln b/TodoApi.sln similarity index 88% rename from TodoApiDTO.sln rename to TodoApi.sln index e49c182b..74d488a4 100644 --- a/TodoApiDTO.sln +++ b/TodoApi.sln @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.30002.166 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApiDTO", "TodoApiDTO.csproj", "{623124F9-F5BA-42DD-BC26-A1720774229C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApi", "TodoApi.csproj", "{623124F9-F5BA-42DD-BC26-A1720774229C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/appsettings.json b/appsettings.json index d9d9a9bf..ea03ff9b 100644 --- a/appsettings.json +++ b/appsettings.json @@ -6,5 +6,27 @@ "Microsoft.Hosting.Lifetime": "Information" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Server=localhost\\SQLEXPRESS;Database=TodoList;Trusted_Connection=True;" + }, + "Serilog": { + "MinimumLevel": { + "Default": "Error", + "Overrides": { + "Microsoft": "Error", + "Microsoft.Hosting.Lifetime": "Error" + } + }, + "Using": ["Serilog.Sinks.File"], + "WriteTo": [ + { + "Name": "File", + "Args": { + "path": "./bin/log.txt", + "rollingInterval": "Day" + } + } + ] + } }