From 404e69a9708ff095795b2337fa1e6344bac00165 Mon Sep 17 00:00:00 2001 From: Nikita Tarasenko Date: Thu, 13 Apr 2023 19:35:00 +0400 Subject: [PATCH] Refactored solution (extracted BLL and DAL). Added swagger. Added logging (separated EF logs from others). InMemoryDB replaced by SQL Server. --- Controllers/TodoItemsController.cs | 116 ------------------ Models/TodoContext.cs | 14 --- Models/TodoItem.cs | 12 -- Program.cs | 26 ---- Startup.cs | 55 --------- .../MappingProfiles/TodoItemMappingProfile.cs | 18 +++ TodoApi.BLL/Extensions/StartupExtension.cs | 17 +++ {Models => TodoApi.BLL/Models}/TodoItemDTO.cs | 9 +- .../Services/Contracts/IServiceTodoItem.cs | 16 +++ TodoApi.BLL/Services/ServiceTodoItem.cs | 50 ++++++++ TodoApi.BLL/TodoApi.BLL.csproj | 19 +++ TodoApi.DAL/DataContext/TodoApiContext.cs | 35 ++++++ .../20230413172447_AddTodoItem.Designer.cs | 53 ++++++++ .../Migrations/20230413172447_AddTodoItem.cs | 36 ++++++ .../Migrations/TodoApiContextModelSnapshot.cs | 50 ++++++++ TodoApi.DAL/Models/BaseEntity.cs | 6 + TodoApi.DAL/Models/TodoItemEntity.cs | 9 ++ .../Repositories/Contracts/IRepository.cs | 11 ++ TodoApi.DAL/Repositories/Repository.cs | 70 +++++++++++ TodoApi.DAL/StartupExtension.cs | 22 ++++ TodoApi.DAL/TodoApi.DAL.csproj | 27 ++++ TodoApi/Controllers/TodoItemsController.cs | 73 +++++++++++ .../ExceptionHandlerMiddleware.cs | 39 ++++++ TodoApi/Program.cs | 22 ++++ .../Properties}/launchSettings.json | 2 +- TodoApi/Startup.cs | 66 ++++++++++ TodoApi/TodoApi.csproj | 23 ++++ .../appsettings.Development.json | 0 appsettings.json => TodoApi/appsettings.json | 5 +- TodoApiDTO.csproj | 18 --- TodoApiDTO.sln | 22 +++- 31 files changed, 689 insertions(+), 252 deletions(-) delete mode 100644 Controllers/TodoItemsController.cs delete mode 100644 Models/TodoContext.cs delete mode 100644 Models/TodoItem.cs delete mode 100644 Program.cs delete mode 100644 Startup.cs create mode 100644 TodoApi.BLL/Extensions/MappingProfiles/TodoItemMappingProfile.cs create mode 100644 TodoApi.BLL/Extensions/StartupExtension.cs rename {Models => TodoApi.BLL/Models}/TodoItemDTO.cs (60%) create mode 100644 TodoApi.BLL/Services/Contracts/IServiceTodoItem.cs create mode 100644 TodoApi.BLL/Services/ServiceTodoItem.cs create mode 100644 TodoApi.BLL/TodoApi.BLL.csproj create mode 100644 TodoApi.DAL/DataContext/TodoApiContext.cs create mode 100644 TodoApi.DAL/Migrations/20230413172447_AddTodoItem.Designer.cs create mode 100644 TodoApi.DAL/Migrations/20230413172447_AddTodoItem.cs create mode 100644 TodoApi.DAL/Migrations/TodoApiContextModelSnapshot.cs create mode 100644 TodoApi.DAL/Models/BaseEntity.cs create mode 100644 TodoApi.DAL/Models/TodoItemEntity.cs create mode 100644 TodoApi.DAL/Repositories/Contracts/IRepository.cs create mode 100644 TodoApi.DAL/Repositories/Repository.cs create mode 100644 TodoApi.DAL/StartupExtension.cs create mode 100644 TodoApi.DAL/TodoApi.DAL.csproj create mode 100644 TodoApi/Controllers/TodoItemsController.cs create mode 100644 TodoApi/ExceptionHandlers/ExceptionHandlerMiddleware.cs create mode 100644 TodoApi/Program.cs rename {Properties => TodoApi/Properties}/launchSettings.json (96%) create mode 100644 TodoApi/Startup.cs create mode 100644 TodoApi/TodoApi.csproj rename appsettings.Development.json => TodoApi/appsettings.Development.json (100%) rename appsettings.json => TodoApi/appsettings.json (54%) delete mode 100644 TodoApiDTO.csproj diff --git a/Controllers/TodoItemsController.cs b/Controllers/TodoItemsController.cs deleted file mode 100644 index 0ef138e7..00000000 --- a/Controllers/TodoItemsController.cs +++ /dev/null @@ -1,116 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using TodoApi.Models; - -namespace TodoApi.Controllers -{ - [Route("api/[controller]")] - [ApiController] - public class TodoItemsController : ControllerBase - { - private readonly TodoContext _context; - - public TodoItemsController(TodoContext context) - { - _context = context; - } - - [HttpGet] - public async Task>> GetTodoItems() - { - return await _context.TodoItems - .Select(x => ItemToDTO(x)) - .ToListAsync(); - } - - [HttpGet("{id}")] - public async Task> GetTodoItem(long id) - { - var todoItem = await _context.TodoItems.FindAsync(id); - - if (todoItem == null) - { - return NotFound(); - } - - return ItemToDTO(todoItem); - } - - [HttpPut("{id}")] - public async Task UpdateTodoItem(long id, TodoItemDTO todoItemDTO) - { - if (id != todoItemDTO.Id) - { - return BadRequest(); - } - - 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)) - { - return NotFound(); - } - - return NoContent(); - } - - [HttpPost] - public async Task> CreateTodoItem(TodoItemDTO todoItemDTO) - { - var todoItem = new TodoItem - { - IsComplete = todoItemDTO.IsComplete, - Name = todoItemDTO.Name - }; - - _context.TodoItems.Add(todoItem); - await _context.SaveChangesAsync(); - - return CreatedAtAction( - nameof(GetTodoItem), - new { id = todoItem.Id }, - ItemToDTO(todoItem)); - } - - [HttpDelete("{id}")] - public async Task DeleteTodoItem(long id) - { - var todoItem = await _context.TodoItems.FindAsync(id); - - if (todoItem == null) - { - 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/Models/TodoContext.cs deleted file mode 100644 index 6e59e363..00000000 --- a/Models/TodoContext.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace TodoApi.Models -{ - public class TodoContext : DbContext - { - public TodoContext(DbContextOptions options) - : base(options) - { - } - - public DbSet TodoItems { get; set; } - } -} \ No newline at end of file diff --git a/Models/TodoItem.cs b/Models/TodoItem.cs deleted file mode 100644 index 1f6e5465..00000000 --- a/Models/TodoItem.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace TodoApi.Models -{ - #region snippet - public class TodoItem - { - public long Id { get; set; } - public string Name { get; set; } - public bool IsComplete { get; set; } - public string Secret { get; set; } - } - #endregion -} \ No newline at end of file diff --git a/Program.cs b/Program.cs deleted file mode 100644 index b27ac16a..00000000 --- a/Program.cs +++ /dev/null @@ -1,26 +0,0 @@ -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; - -namespace TodoApi -{ - public class Program - { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - } -} diff --git a/Startup.cs b/Startup.cs deleted file mode 100644 index bbfbc83d..00000000 --- a/Startup.cs +++ /dev/null @@ -1,55 +0,0 @@ -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; - -namespace TodoApi -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddDbContext(opt => - opt.UseInMemoryDatabase("TodoList")); - services.AddControllers(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseHttpsRedirection(); - - app.UseRouting(); - - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } - } -} diff --git a/TodoApi.BLL/Extensions/MappingProfiles/TodoItemMappingProfile.cs b/TodoApi.BLL/Extensions/MappingProfiles/TodoItemMappingProfile.cs new file mode 100644 index 00000000..e8dc1472 --- /dev/null +++ b/TodoApi.BLL/Extensions/MappingProfiles/TodoItemMappingProfile.cs @@ -0,0 +1,18 @@ +using AutoMapper; +using TodoApi.BLL.Models; +using TodoApi.DAL.Models; + +namespace TodoApi.BLL.Extensions.MappingProfiles; + +public class TodoItemMappingProfile : Profile +{ + public TodoItemMappingProfile() + { + CreateMap() + .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id)) + .ForMember(dest => dest.IsComplete, opt => opt.MapFrom(src => src.IsComplete)) + .ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name)) + .ForMember(dest => dest.Secret, opt => opt.Ignore()) + .ReverseMap(); + } +} \ No newline at end of file diff --git a/TodoApi.BLL/Extensions/StartupExtension.cs b/TodoApi.BLL/Extensions/StartupExtension.cs new file mode 100644 index 00000000..2ec24a3e --- /dev/null +++ b/TodoApi.BLL/Extensions/StartupExtension.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using TodoApi.BLL.Extensions.MappingProfiles; +using TodoApi.BLL.Services; +using TodoApi.BLL.Services.Contracts; + +namespace TodoApi.BLL.Extensions; + +public static class StartupExtension +{ + public static void AddBLLServices(this IServiceCollection services, IConfiguration configuration) + { + services.AddScoped(); + + services.AddAutoMapper(typeof(TodoItemMappingProfile)); + } +} \ No newline at end of file diff --git a/Models/TodoItemDTO.cs b/TodoApi.BLL/Models/TodoItemDTO.cs similarity index 60% rename from Models/TodoItemDTO.cs rename to TodoApi.BLL/Models/TodoItemDTO.cs index e66a500a..41a3ba1e 100644 --- a/Models/TodoItemDTO.cs +++ b/TodoApi.BLL/Models/TodoItemDTO.cs @@ -1,11 +1,12 @@ -namespace TodoApi.Models +namespace TodoApi.BLL.Models { - #region snippet - public class TodoItemDTO + public class TodoItemDto { + public long Id { get; set; } + public string Name { get; set; } + public bool IsComplete { get; set; } } - #endregion } diff --git a/TodoApi.BLL/Services/Contracts/IServiceTodoItem.cs b/TodoApi.BLL/Services/Contracts/IServiceTodoItem.cs new file mode 100644 index 00000000..1fd5bfb6 --- /dev/null +++ b/TodoApi.BLL/Services/Contracts/IServiceTodoItem.cs @@ -0,0 +1,16 @@ +using TodoApi.BLL.Models; + +namespace TodoApi.BLL.Services.Contracts; + +public interface IServiceTodoItem +{ + public Task> GetTodoItems(); + + public Task GetTodoItemById(long id); + + public Task UpdateTodoItem(TodoItemDto todoItemDto); + + public Task CreateTodoItem(TodoItemDto todoItemDto); + + public Task DeleteTodoItem(long id); +} \ No newline at end of file diff --git a/TodoApi.BLL/Services/ServiceTodoItem.cs b/TodoApi.BLL/Services/ServiceTodoItem.cs new file mode 100644 index 00000000..0a429c8c --- /dev/null +++ b/TodoApi.BLL/Services/ServiceTodoItem.cs @@ -0,0 +1,50 @@ +using AutoMapper; +using TodoApi.BLL.Models; +using TodoApi.BLL.Services.Contracts; +using TodoApi.DAL.Models; +using TodoApi.DAL.Repositories.Contracts; + +namespace TodoApi.BLL.Services; + +public class ServiceTodoItem : IServiceTodoItem +{ + private readonly IRepository _repository; + private readonly IMapper _mapper; + + public ServiceTodoItem(IRepository repository, IMapper mapper) + { + _repository = repository; + _mapper = mapper; + } + + public async Task> GetTodoItems() + { + var todoItems = await _repository.GetAll(); + + return todoItems.Select(i => _mapper.Map(i)).ToList(); + } + + public async Task GetTodoItemById(long id) + { + var todoItem = await _repository.GetById(id); + + return _mapper.Map(todoItem); + } + + public async Task UpdateTodoItem(TodoItemDto todoItemDto) + { + return await _repository.Update(_mapper.Map(todoItemDto)); + } + + public async Task CreateTodoItem(TodoItemDto todoItemDto) + { + var createdItem = await _repository.Create(_mapper.Map(todoItemDto)); + + return _mapper.Map(createdItem); + } + + public async Task DeleteTodoItem(long id) + { + return await _repository.Delete(id); + } +} \ No newline at end of file diff --git a/TodoApi.BLL/TodoApi.BLL.csproj b/TodoApi.BLL/TodoApi.BLL.csproj new file mode 100644 index 00000000..30601741 --- /dev/null +++ b/TodoApi.BLL/TodoApi.BLL.csproj @@ -0,0 +1,19 @@ + + + + net7.0 + enable + enable + 10 + + + + + + + + + + + + diff --git a/TodoApi.DAL/DataContext/TodoApiContext.cs b/TodoApi.DAL/DataContext/TodoApiContext.cs new file mode 100644 index 00000000..49ad9dc6 --- /dev/null +++ b/TodoApi.DAL/DataContext/TodoApiContext.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using TodoApi.DAL.Models; + +namespace TodoApi.DAL.DataContext; + +public partial class TodoApiContext : DbContext +{ + private readonly ILoggerFactory _loggerFactory; + + public TodoApiContext() + { + } + + public TodoApiContext(DbContextOptions options, ILoggerFactory loggerFactory) + : base(options) + { + _loggerFactory = loggerFactory; + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) +#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see http://go.microsoft.com/fwlink/?LinkId=723263. + => optionsBuilder + .UseLoggerFactory(_loggerFactory) + .UseSqlServer("Server=T1KZZ\\SQLEXPRESS;Database=TodoApi;Trusted_Connection=True;TrustServerCertificate=True"); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + OnModelCreatingPartial(modelBuilder); + } + + partial void OnModelCreatingPartial(ModelBuilder modelBuilder); + + public DbSet TodoItems { get; set; } +} diff --git a/TodoApi.DAL/Migrations/20230413172447_AddTodoItem.Designer.cs b/TodoApi.DAL/Migrations/20230413172447_AddTodoItem.Designer.cs new file mode 100644 index 00000000..9527108d --- /dev/null +++ b/TodoApi.DAL/Migrations/20230413172447_AddTodoItem.Designer.cs @@ -0,0 +1,53 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TodoApi.DAL; +using TodoApi.DAL.DataContext; + +#nullable disable + +namespace TodoApi.DAL.Migrations +{ + [DbContext(typeof(TodoApiContext))] + [Migration("20230413172447_AddTodoItem")] + partial class AddTodoItem + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("TodoApi.DAL.Models.TodoItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("IsComplete") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Secret") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("TodoItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TodoApi.DAL/Migrations/20230413172447_AddTodoItem.cs b/TodoApi.DAL/Migrations/20230413172447_AddTodoItem.cs new file mode 100644 index 00000000..c7920238 --- /dev/null +++ b/TodoApi.DAL/Migrations/20230413172447_AddTodoItem.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TodoApi.DAL.Migrations +{ + /// + public partial class AddTodoItem : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "TodoItems", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: false), + IsComplete = table.Column(type: "bit", nullable: false), + Secret = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_TodoItems", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TodoItems"); + } + } +} diff --git a/TodoApi.DAL/Migrations/TodoApiContextModelSnapshot.cs b/TodoApi.DAL/Migrations/TodoApiContextModelSnapshot.cs new file mode 100644 index 00000000..bfe47b67 --- /dev/null +++ b/TodoApi.DAL/Migrations/TodoApiContextModelSnapshot.cs @@ -0,0 +1,50 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TodoApi.DAL; +using TodoApi.DAL.DataContext; + +#nullable disable + +namespace TodoApi.DAL.Migrations +{ + [DbContext(typeof(TodoApiContext))] + partial class TodoApiContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("TodoApi.DAL.Models.TodoItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("IsComplete") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Secret") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("TodoItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TodoApi.DAL/Models/BaseEntity.cs b/TodoApi.DAL/Models/BaseEntity.cs new file mode 100644 index 00000000..27e871f5 --- /dev/null +++ b/TodoApi.DAL/Models/BaseEntity.cs @@ -0,0 +1,6 @@ +namespace TodoApi.DAL.Models; + +public class BaseEntity +{ + public long Id { get; set; } +} \ No newline at end of file diff --git a/TodoApi.DAL/Models/TodoItemEntity.cs b/TodoApi.DAL/Models/TodoItemEntity.cs new file mode 100644 index 00000000..a9f47123 --- /dev/null +++ b/TodoApi.DAL/Models/TodoItemEntity.cs @@ -0,0 +1,9 @@ +namespace TodoApi.DAL.Models +{ + public class TodoItemEntity : BaseEntity + { + public string Name { get; set; } + public bool IsComplete { get; set; } + public string? Secret { get; set; } + } +} \ No newline at end of file diff --git a/TodoApi.DAL/Repositories/Contracts/IRepository.cs b/TodoApi.DAL/Repositories/Contracts/IRepository.cs new file mode 100644 index 00000000..0ad64255 --- /dev/null +++ b/TodoApi.DAL/Repositories/Contracts/IRepository.cs @@ -0,0 +1,11 @@ +namespace TodoApi.DAL.Repositories.Contracts +{ + public interface IRepository where T : class + { + public Task Create(T obj); + public Task Delete(long id); + public Task Update(T item); + public Task> GetAll(); + public Task GetById(long id); + } +} \ No newline at end of file diff --git a/TodoApi.DAL/Repositories/Repository.cs b/TodoApi.DAL/Repositories/Repository.cs new file mode 100644 index 00000000..73167d45 --- /dev/null +++ b/TodoApi.DAL/Repositories/Repository.cs @@ -0,0 +1,70 @@ +using Microsoft.EntityFrameworkCore; +using TodoApi.DAL.Models; +using TodoApi.DAL.Repositories.Contracts; + +namespace TodoApi.DAL.Repositories; + +public class Repository : IRepository + where DbC : DbContext + where T : BaseEntity, new() +{ + private readonly DbC _dbContext; + + public Repository(DbC todoContext) + { + _dbContext = todoContext; + } + + private bool IsExists(long id) => + _dbContext.Set().Any(e => e.Id == id); + + public async Task Create(T item) + { + var obj = await _dbContext.Set().AddAsync(item); + await _dbContext.SaveChangesAsync(); + return obj.Entity; + } + + public async Task Delete(long id) + { + if (!IsExists(id)) + { + return false; + } + + _dbContext.Set().Remove(new T { Id = id }); + await _dbContext.SaveChangesAsync(); + + return true; + } + + public async Task Update(T changedItem) + { + if (!IsExists(changedItem.Id)) + { + return false; + } + + try + { + _dbContext.Set().Update(changedItem); + await _dbContext.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) when (!IsExists(changedItem.Id)) + { + return false; + } + + return true; + } + + public async Task> GetAll() + { + return _dbContext.Set(); + } + + public async Task GetById(long id) + { + return await _dbContext.Set().FindAsync(id); + } +} \ No newline at end of file diff --git a/TodoApi.DAL/StartupExtension.cs b/TodoApi.DAL/StartupExtension.cs new file mode 100644 index 00000000..0cccbf9d --- /dev/null +++ b/TodoApi.DAL/StartupExtension.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using TodoApi.DAL.DataContext; +using TodoApi.DAL.Models; +using TodoApi.DAL.Repositories; +using TodoApi.DAL.Repositories.Contracts; + +namespace TodoApi.DAL; + +public static class StartupExtension +{ + public static void AddDALServices(this IServiceCollection services, IConfiguration configuration) + { + services.AddDbContext(options => + { + options.UseSqlServer(configuration.GetConnectionString(nameof(TodoApiContext))); + }); + + services.AddScoped, Repository>(); + } +} \ No newline at end of file diff --git a/TodoApi.DAL/TodoApi.DAL.csproj b/TodoApi.DAL/TodoApi.DAL.csproj new file mode 100644 index 00000000..155edd32 --- /dev/null +++ b/TodoApi.DAL/TodoApi.DAL.csproj @@ -0,0 +1,27 @@ + + + + net7.0 + enable + enable + 10 + TodoApi.DAL + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/TodoApi/Controllers/TodoItemsController.cs b/TodoApi/Controllers/TodoItemsController.cs new file mode 100644 index 00000000..961fea0b --- /dev/null +++ b/TodoApi/Controllers/TodoItemsController.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using TodoApi.BLL.Models; +using TodoApi.BLL.Services.Contracts; + +namespace TodoApi.Controllers; + +[Route("api/[controller]")] +[ApiController] +public class TodoItemsController : ControllerBase +{ + private readonly IServiceTodoItem _service; + + public TodoItemsController(IServiceTodoItem service) + { + _service = service; + } + + [HttpGet] + public async Task>> GetTodoItems() + { + var todoItems = await _service.GetTodoItems(); + return Ok(todoItems); + } + + [HttpGet("{id}")] + public async Task> GetTodoItem(long id) + { + var todoItem = await _service.GetTodoItemById(id); + + if (todoItem == null) + { + return NotFound(); + } + + return Ok(todoItem); + } + + [HttpPut("{id}")] + public async Task UpdateTodoItem(long id, TodoItemDto todoItemDTO) + { + todoItemDTO.Id = id; + + var updateResult = await _service.UpdateTodoItem(todoItemDTO); + + if (!updateResult) + { + return NotFound(); + } + + return NoContent(); + } + + [HttpPost] + public async Task> CreateTodoItem(TodoItemDto todoItemDTO) + { + return await _service.CreateTodoItem(todoItemDTO); + } + + [HttpDelete("{id}")] + public async Task DeleteTodoItem(long id) + { + var deleteIsSuccess = await _service.DeleteTodoItem(id); + + if (!deleteIsSuccess) + { + return NotFound(); + } + + return NoContent(); + } +} \ No newline at end of file diff --git a/TodoApi/ExceptionHandlers/ExceptionHandlerMiddleware.cs b/TodoApi/ExceptionHandlers/ExceptionHandlerMiddleware.cs new file mode 100644 index 00000000..595d8666 --- /dev/null +++ b/TodoApi/ExceptionHandlers/ExceptionHandlerMiddleware.cs @@ -0,0 +1,39 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace TodoApi.ExceptionHandlers; + +public class ExceptionHandlerMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public ExceptionHandlerMiddleware(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + + public async Task InvokeAsync(HttpContext httpContext) + { + try + { + _logger.LogInformation($"httpContext: {httpContext}"); + await _next(httpContext); + } + catch (Exception ex) + { + _logger.LogError($"Something went wrong: {ex}"); + await HandleException(httpContext, ex); + } + } + private async Task HandleException(HttpContext context, Exception exception) + { + context.Response.ContentType = "application/json"; + context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + await context.Response.WriteAsync("Something went wrong."); + } +} \ No newline at end of file diff --git a/TodoApi/Program.cs b/TodoApi/Program.cs new file mode 100644 index 00000000..fd1458c3 --- /dev/null +++ b/TodoApi/Program.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +using NLog; + +namespace TodoApi; + +public class Program +{ + public static void Main(string[] args) + { + /*var logger = LogManager.GetCurrentClassLogger(); + logger.Info("Starting application...");*/ + + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); + } +} \ No newline at end of file diff --git a/Properties/launchSettings.json b/TodoApi/Properties/launchSettings.json similarity index 96% rename from Properties/launchSettings.json rename to TodoApi/Properties/launchSettings.json index 6766196a..73f9c651 100644 --- a/Properties/launchSettings.json +++ b/TodoApi/Properties/launchSettings.json @@ -15,7 +15,7 @@ "ASPNETCORE_ENVIRONMENT": "Development" } }, - "TodoApiDTO": { + "TodoApi": { "commandName": "Project", "launchBrowser": true, "environmentVariables": { diff --git a/TodoApi/Startup.cs b/TodoApi/Startup.cs new file mode 100644 index 00000000..c26c4b7b --- /dev/null +++ b/TodoApi/Startup.cs @@ -0,0 +1,66 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.OpenApi.Models; +using NLog.Extensions.Logging; +using TodoApi.BLL.Extensions; +using TodoApi.DAL; +using TodoApi.ExceptionHandlers; + +namespace TodoApi; + +public class Startup +{ + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddLogging(loggingBuilder => + { + loggingBuilder.ClearProviders(); + loggingBuilder.AddNLog(Configuration); + }); + + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo { Title = "v1", Version = "v1" }); + }); + + services.AddControllers(); + + services.AddBLLServices(Configuration); + services.AddDALServices(Configuration); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) app.UseDeveloperExceptionPage(); + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseSwagger(); + app.UseSwaggerUI(options => + { + options.SwaggerEndpoint("/swagger/v1/swagger.json", "v1"); + options.RoutePrefix = string.Empty; + }); + + app.UseMiddleware(); + + app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); + } +} \ No newline at end of file diff --git a/TodoApi/TodoApi.csproj b/TodoApi/TodoApi.csproj new file mode 100644 index 00000000..341eb816 --- /dev/null +++ b/TodoApi/TodoApi.csproj @@ -0,0 +1,23 @@ + + + + net7.0 + TodoApi + + + + + + + + + + + + + + + + + + diff --git a/appsettings.Development.json b/TodoApi/appsettings.Development.json similarity index 100% rename from appsettings.Development.json rename to TodoApi/appsettings.Development.json diff --git a/appsettings.json b/TodoApi/appsettings.json similarity index 54% rename from appsettings.json rename to TodoApi/appsettings.json index d9d9a9bf..ee50379e 100644 --- a/appsettings.json +++ b/TodoApi/appsettings.json @@ -6,5 +6,8 @@ "Microsoft.Hosting.Lifetime": "Information" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "ConnectionStrings": { + "TodoApiContext": "Server=localhost;Database=TodoApi;Trusted_Connection=True;" + } } diff --git a/TodoApiDTO.csproj b/TodoApiDTO.csproj deleted file mode 100644 index bba6f6af..00000000 --- a/TodoApiDTO.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - netcoreapp3.1 - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - diff --git a/TodoApiDTO.sln b/TodoApiDTO.sln index e49c182b..18d5cd25 100644 --- a/TodoApiDTO.sln +++ b/TodoApiDTO.sln @@ -3,7 +3,11 @@ 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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TodoApi.DAL", "TodoApi.DAL\TodoApi.DAL.csproj", "{7E474E1B-A1F8-4F86-B709-DE219F1D7188}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TodoApi.BLL", "TodoApi.BLL\TodoApi.BLL.csproj", "{96AC8460-1D47-4D2F-BF05-26DA6E27FEF2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TodoApi", "TodoApi\TodoApi.csproj", "{D23351B2-728F-4661-A968-4B0AAB4A7F6F}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -11,10 +15,18 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {623124F9-F5BA-42DD-BC26-A1720774229C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {623124F9-F5BA-42DD-BC26-A1720774229C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {623124F9-F5BA-42DD-BC26-A1720774229C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {623124F9-F5BA-42DD-BC26-A1720774229C}.Release|Any CPU.Build.0 = Release|Any CPU + {7E474E1B-A1F8-4F86-B709-DE219F1D7188}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7E474E1B-A1F8-4F86-B709-DE219F1D7188}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7E474E1B-A1F8-4F86-B709-DE219F1D7188}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7E474E1B-A1F8-4F86-B709-DE219F1D7188}.Release|Any CPU.Build.0 = Release|Any CPU + {96AC8460-1D47-4D2F-BF05-26DA6E27FEF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {96AC8460-1D47-4D2F-BF05-26DA6E27FEF2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {96AC8460-1D47-4D2F-BF05-26DA6E27FEF2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {96AC8460-1D47-4D2F-BF05-26DA6E27FEF2}.Release|Any CPU.Build.0 = Release|Any CPU + {D23351B2-728F-4661-A968-4B0AAB4A7F6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D23351B2-728F-4661-A968-4B0AAB4A7F6F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D23351B2-728F-4661-A968-4B0AAB4A7F6F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D23351B2-728F-4661-A968-4B0AAB4A7F6F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE