diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..9a980d97 Binary files /dev/null and b/.DS_Store differ 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/Models/TodoItemDTO.cs b/Models/TodoItemDTO.cs deleted file mode 100644 index e66a500a..00000000 --- a/Models/TodoItemDTO.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace TodoApi.Models -{ - #region snippet - public class TodoItemDTO - { - public long Id { get; set; } - public string Name { get; set; } - public bool IsComplete { get; set; } - } - #endregion -} diff --git a/Todo.API/Controllers/TodoItemsController.cs b/Todo.API/Controllers/TodoItemsController.cs new file mode 100644 index 00000000..dd695641 --- /dev/null +++ b/Todo.API/Controllers/TodoItemsController.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using AutoMapper; +using Microsoft.AspNetCore.Mvc; +using Todo.Core.Exceptions; +using Todo.Core.Interfaces; +using Todo.Core.Models.TodoItem; +using Todo.Infrastructure.Entities; + +namespace Todo.API.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class TodoItemsController : ControllerBase + { + private readonly IItemService _itemService; + private readonly IMapper _mapper; + + public TodoItemsController(IItemService itemService, IMapper mapper) + { + _itemService = itemService; + _mapper = mapper; + } + + [HttpGet] + public async Task>> GetTodoItems() + { + try + { + var result = await _itemService.GetAll(); + + return Ok(_mapper.Map>(result)); + } + catch (Exception ex) + { + var errorDetails = new ErrorDetails(500, ex.Message); + return StatusCode(500, errorDetails); + } + } + + [HttpGet("{id}")] + public async Task> GetTodoItem(long id) + { + try + { + var item = await _itemService.Read(id); + + return Ok(_mapper.Map(item)); + } + catch (Exception e) + { + Console.WriteLine(e); + return BadRequest(); + } + } + + [HttpPut("{id}")] + public async Task UpdateTodoItem(long id, [FromBody] TodoItemDTOUpdate todoItemDTO) + { + try + { + var item = await TodoItemExists(id); + _mapper.Map(todoItemDTO, item); + + await _itemService.Update(item); + + return NoContent(); + } + catch (Exception e) + { + Console.WriteLine(e); + return BadRequest(); + } + } + + [HttpPost] + public async Task> CreateTodoItem([FromBody] TodoItemDTOCreate todoItemDTO) + { + try + { + var toEntity = _mapper.Map(todoItemDTO); + await _itemService.Create(toEntity); + + var createdItem = _mapper.Map(toEntity); + + return CreatedAtRoute("TodoItemById", new { todoItemId = createdItem.Id }, createdItem); + } + catch (Exception e) + { + Console.WriteLine(e); + return BadRequest(); + } + } + + [HttpDelete("{id}")] + public async Task DeleteTodoItem(long id) + { + try + { + var item = await TodoItemExists(id); + + await _itemService.Delete(item); + + return NoContent(); + } + catch (Exception e) + { + Console.WriteLine(e.ToString()); + return BadRequest(); + } + } + + private async Task TodoItemExists(long id) + { + var item = await _itemService.Read(id); + + return item; + } + } +} \ No newline at end of file diff --git a/Program.cs b/Todo.API/Program.cs similarity index 100% rename from Program.cs rename to Todo.API/Program.cs diff --git a/Properties/launchSettings.json b/Todo.API/Properties/launchSettings.json similarity index 91% rename from Properties/launchSettings.json rename to Todo.API/Properties/launchSettings.json index 6766196a..cf5e46de 100644 --- a/Properties/launchSettings.json +++ b/Todo.API/Properties/launchSettings.json @@ -11,6 +11,7 @@ "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, + "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -18,6 +19,7 @@ "TodoApiDTO": { "commandName": "Project", "launchBrowser": true, + "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, diff --git a/README.md b/Todo.API/README.md similarity index 100% rename from README.md rename to Todo.API/README.md diff --git a/Startup.cs b/Todo.API/Startup.cs similarity index 51% rename from Startup.cs rename to Todo.API/Startup.cs index bbfbc83d..92433388 100644 --- a/Startup.cs +++ b/Todo.API/Startup.cs @@ -1,17 +1,17 @@ -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.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using TodoApi.Models; +using Todo.Core.Interfaces; +using Todo.Core.Mappings; +using Todo.Core.Services; +using Todo.Infrastructure.DbContexts; +using Todo.Infrastructure.Entities; +using Todo.Infrastructure.Interfaces; +using Todo.Infrastructure.Repositories; namespace TodoApi { @@ -27,8 +27,14 @@ 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.AddDbContext(opt => - opt.UseInMemoryDatabase("TodoList")); + services.AddTransient, Repository>(); + services.AddTransient(); + services.AddSwaggerGen(); + services.AddAutoMapper(typeof(MappingProfile)); + + services.AddDbContext(options => + options.UseSqlServer(Configuration.GetConnectionString("Sql"))); + services.AddControllers(); } @@ -49,7 +55,18 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseEndpoints(endpoints => { endpoints.MapControllers(); + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + + endpoints.MapControllerRoute( + name: "TodoItemById", + pattern: "api/todo/{todoItemId}", + defaults: new { controller = "TodoItems", action = "GetTodoItem" }); }); + + app.UseSwagger(); + app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "My Test1 Api v1"); }); } } -} +} \ No newline at end of file diff --git a/Todo.API/Todo.API.csproj b/Todo.API/Todo.API.csproj new file mode 100644 index 00000000..78250287 --- /dev/null +++ b/Todo.API/Todo.API.csproj @@ -0,0 +1,22 @@ + + + + net7.0 + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/appsettings.Development.json b/Todo.API/appsettings.Development.json similarity index 100% rename from appsettings.Development.json rename to Todo.API/appsettings.Development.json diff --git a/Todo.API/appsettings.json b/Todo.API/appsettings.json new file mode 100644 index 00000000..d35eaa4d --- /dev/null +++ b/Todo.API/appsettings.json @@ -0,0 +1,13 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "Sql": "Server=localhost;Database=Account;User Id=sa;Password=Password.1;ApplicationIntent=ReadWrite;MultiSubnetFailover=False; TrustServerCertificate=True" + } +} diff --git a/Todo.Core/Exceptions/ErrorDetails.cs b/Todo.Core/Exceptions/ErrorDetails.cs new file mode 100644 index 00000000..3a537981 --- /dev/null +++ b/Todo.Core/Exceptions/ErrorDetails.cs @@ -0,0 +1,13 @@ +namespace Todo.Core.Exceptions; + +public class ErrorDetails +{ + public int StatusCode { get; set; } + public string Message { get; set; } + + public ErrorDetails(int statusCode, string message) + { + StatusCode = statusCode; + Message = message; + } +} \ No newline at end of file diff --git a/Todo.Core/Exceptions/ItemNotFoundException.cs b/Todo.Core/Exceptions/ItemNotFoundException.cs new file mode 100644 index 00000000..bbbf8205 --- /dev/null +++ b/Todo.Core/Exceptions/ItemNotFoundException.cs @@ -0,0 +1,13 @@ +namespace Todo.Core.Exceptions; + +public class ItemNotFoundException : Exception +{ + public ItemNotFoundException(string message) : base(message) + { + } + + public ErrorDetails ToErrorDetails() + { + return new ErrorDetails(404, Message); // 404 indicates "Not Found" status + } +} \ No newline at end of file diff --git a/Todo.Core/Interfaces/IItemService.cs b/Todo.Core/Interfaces/IItemService.cs new file mode 100644 index 00000000..4a4e38b5 --- /dev/null +++ b/Todo.Core/Interfaces/IItemService.cs @@ -0,0 +1,16 @@ +using Todo.Infrastructure.Entities; + +namespace Todo.Core.Interfaces; + +public interface IItemService +{ + Task Create(TodoItem item); + + Task Read(long id); + + Task Update(TodoItem item); + + Task Delete(TodoItem item); + + Task> GetAll(); +} \ No newline at end of file diff --git a/Todo.Core/Mappings/MappingProfile.cs b/Todo.Core/Mappings/MappingProfile.cs new file mode 100644 index 00000000..e5ade854 --- /dev/null +++ b/Todo.Core/Mappings/MappingProfile.cs @@ -0,0 +1,16 @@ +using AutoMapper; +using Todo.Core.Models; +using Todo.Core.Models.TodoItem; +using Todo.Infrastructure.Entities; + +namespace Todo.Core.Mappings; + +public class MappingProfile : Profile +{ + public MappingProfile() + { + CreateMap(); + CreateMap(); + CreateMap(); + } +} \ No newline at end of file diff --git a/Todo.Core/Models/TodoItem/TodoItemDTO.cs b/Todo.Core/Models/TodoItem/TodoItemDTO.cs new file mode 100644 index 00000000..8297a03a --- /dev/null +++ b/Todo.Core/Models/TodoItem/TodoItemDTO.cs @@ -0,0 +1,10 @@ +namespace Todo.Core.Models.TodoItem; + +public class TodoItemDTO +{ + public long Id { get; set; } + + public string Name { get; set; } + + public bool IsComplete { get; set; } +} diff --git a/Todo.Core/Models/TodoItem/TodoItemDTOCreate.cs b/Todo.Core/Models/TodoItem/TodoItemDTOCreate.cs new file mode 100644 index 00000000..11446e94 --- /dev/null +++ b/Todo.Core/Models/TodoItem/TodoItemDTOCreate.cs @@ -0,0 +1,5 @@ +namespace Todo.Core.Models.TodoItem; + +public class TodoItemDTOCreate : TodoItemDtoManipulation +{ +} \ No newline at end of file diff --git a/Todo.Core/Models/TodoItem/TodoItemDTOManipulation.cs b/Todo.Core/Models/TodoItem/TodoItemDTOManipulation.cs new file mode 100644 index 00000000..1416dbff --- /dev/null +++ b/Todo.Core/Models/TodoItem/TodoItemDTOManipulation.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace Todo.Core.Models.TodoItem; + +public class TodoItemDtoManipulation +{ + [Required] + public string Name { get; init; } + + [Required] + public bool IsComplete { get; init; } +} \ No newline at end of file diff --git a/Todo.Core/Models/TodoItem/TodoItemDTOUpdate.cs b/Todo.Core/Models/TodoItem/TodoItemDTOUpdate.cs new file mode 100644 index 00000000..d6899c74 --- /dev/null +++ b/Todo.Core/Models/TodoItem/TodoItemDTOUpdate.cs @@ -0,0 +1,6 @@ +namespace Todo.Core.Models.TodoItem; + +public class TodoItemDTOUpdate : TodoItemDtoManipulation +{ + +} \ No newline at end of file diff --git a/Todo.Core/Services/ItemService.cs b/Todo.Core/Services/ItemService.cs new file mode 100644 index 00000000..dd7d2048 --- /dev/null +++ b/Todo.Core/Services/ItemService.cs @@ -0,0 +1,43 @@ +using Todo.Core.Interfaces; +using Todo.Infrastructure.Entities; +using Todo.Infrastructure.Interfaces; + +namespace Todo.Core.Services; + +public class ItemService : IItemService +{ + private readonly IRepository _repository; + + public ItemService(IRepository repository) + { + _repository = repository; + } + + public async Task Create(TodoItem item) + { + _repository.Create(item); + await _repository.SaveChanges(); + } + + public async Task Read(long id) + { + return await _repository.Read(x => x.Id == id); + } + + public async Task Update(TodoItem item) + { + _repository.Update(item); + await _repository.SaveChanges(); + } + + public async Task Delete(TodoItem item) + { + _repository.Delete(item); + await _repository.SaveChanges(); + } + + public async Task> GetAll() + { + return await _repository.GetAll(); + } +} \ No newline at end of file diff --git a/Todo.Core/Todo.Core.csproj b/Todo.Core/Todo.Core.csproj new file mode 100644 index 00000000..6b59291c --- /dev/null +++ b/Todo.Core/Todo.Core.csproj @@ -0,0 +1,17 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + diff --git a/Todo.Infrastructure/DbContexts/TodoDbContext.cs b/Todo.Infrastructure/DbContexts/TodoDbContext.cs new file mode 100644 index 00000000..a1e94a5e --- /dev/null +++ b/Todo.Infrastructure/DbContexts/TodoDbContext.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore; +using Todo.Infrastructure.Entities; + +namespace Todo.Infrastructure.DbContexts; + +public class TodoDbContext : DbContext +{ + public TodoDbContext() + { + } + public TodoDbContext(DbContextOptions options) + : base(options) + { + } + + public DbSet TodoItems { get; set; } +} \ No newline at end of file diff --git a/Todo.Infrastructure/Entities/TodoItem.cs b/Todo.Infrastructure/Entities/TodoItem.cs new file mode 100644 index 00000000..38fe2a92 --- /dev/null +++ b/Todo.Infrastructure/Entities/TodoItem.cs @@ -0,0 +1,14 @@ +using Microsoft.EntityFrameworkCore; + +namespace Todo.Infrastructure.Entities; + +public class TodoItem +{ + public long Id { get; set; } + + public string? Name { get; set; } + + public bool IsComplete { get; set; } + + public string? Secret { get; set; } +} \ No newline at end of file diff --git a/Todo.Infrastructure/Interfaces/IRepository.cs b/Todo.Infrastructure/Interfaces/IRepository.cs new file mode 100644 index 00000000..eb0ef14f --- /dev/null +++ b/Todo.Infrastructure/Interfaces/IRepository.cs @@ -0,0 +1,19 @@ +using System.Linq.Expressions; + +namespace Todo.Infrastructure.Interfaces; + +public interface IRepository + where T : class +{ + void Create(T entity); + + Task Read(Expression> condition); + + void Update(T entity); + + void Delete(T entity); + + Task SaveChanges(); + + Task> GetAll(); +} \ No newline at end of file diff --git a/Todo.Infrastructure/Migrations/20230818113629_YourMigrationName.Designer.cs b/Todo.Infrastructure/Migrations/20230818113629_YourMigrationName.Designer.cs new file mode 100644 index 00000000..0b3ba995 --- /dev/null +++ b/Todo.Infrastructure/Migrations/20230818113629_YourMigrationName.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 Todo.Infrastructure.DbContexts; + +#nullable disable + +namespace Todo.Infrastructure.Migrations +{ + [DbContext(typeof(TodoDbContext))] + [Migration("20230818113629_YourMigrationName")] + partial class YourMigrationName + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Todo.Infrastructure.Entities.TodoItem", 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") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("TodoItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Todo.Infrastructure/Migrations/20230818113629_YourMigrationName.cs b/Todo.Infrastructure/Migrations/20230818113629_YourMigrationName.cs new file mode 100644 index 00000000..b0c83015 --- /dev/null +++ b/Todo.Infrastructure/Migrations/20230818113629_YourMigrationName.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Todo.Infrastructure.Migrations +{ + /// + public partial class YourMigrationName : 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: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TodoItems", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TodoItems"); + } + } +} diff --git a/Todo.Infrastructure/Migrations/20230818114606_updateTable.Designer.cs b/Todo.Infrastructure/Migrations/20230818114606_updateTable.Designer.cs new file mode 100644 index 00000000..884c28ab --- /dev/null +++ b/Todo.Infrastructure/Migrations/20230818114606_updateTable.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 Todo.Infrastructure.DbContexts; + +#nullable disable + +namespace Todo.Infrastructure.Migrations +{ + [DbContext(typeof(TodoDbContext))] + [Migration("20230818114606_updateTable")] + partial class updateTable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Todo.Infrastructure.Entities.TodoItem", 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") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("TodoItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Todo.Infrastructure/Migrations/20230818114606_updateTable.cs b/Todo.Infrastructure/Migrations/20230818114606_updateTable.cs new file mode 100644 index 00000000..d084700e --- /dev/null +++ b/Todo.Infrastructure/Migrations/20230818114606_updateTable.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Todo.Infrastructure.Migrations +{ + /// + public partial class updateTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/Todo.Infrastructure/Migrations/20230820174520_updateColumn.Designer.cs b/Todo.Infrastructure/Migrations/20230820174520_updateColumn.Designer.cs new file mode 100644 index 00000000..f5543d3a --- /dev/null +++ b/Todo.Infrastructure/Migrations/20230820174520_updateColumn.Designer.cs @@ -0,0 +1,51 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Todo.Infrastructure.DbContexts; + +#nullable disable + +namespace Todo.Infrastructure.Migrations +{ + [DbContext(typeof(TodoDbContext))] + [Migration("20230820174520_updateColumn")] + partial class updateColumn + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Todo.Infrastructure.Entities.TodoItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + 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/Todo.Infrastructure/Migrations/20230820174520_updateColumn.cs b/Todo.Infrastructure/Migrations/20230820174520_updateColumn.cs new file mode 100644 index 00000000..c4027bba --- /dev/null +++ b/Todo.Infrastructure/Migrations/20230820174520_updateColumn.cs @@ -0,0 +1,54 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Todo.Infrastructure.Migrations +{ + /// + public partial class updateColumn : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Secret", + table: "TodoItems", + type: "nvarchar(max)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(max)"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "TodoItems", + type: "nvarchar(max)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(max)"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Secret", + table: "TodoItems", + type: "nvarchar(max)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(max)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Name", + table: "TodoItems", + type: "nvarchar(max)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(max)", + oldNullable: true); + } + } +} diff --git a/Todo.Infrastructure/Migrations/TodoDbContextModelSnapshot.cs b/Todo.Infrastructure/Migrations/TodoDbContextModelSnapshot.cs new file mode 100644 index 00000000..3df0e315 --- /dev/null +++ b/Todo.Infrastructure/Migrations/TodoDbContextModelSnapshot.cs @@ -0,0 +1,48 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Todo.Infrastructure.DbContexts; + +#nullable disable + +namespace Todo.Infrastructure.Migrations +{ + [DbContext(typeof(TodoDbContext))] + partial class TodoDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Todo.Infrastructure.Entities.TodoItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + 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/Todo.Infrastructure/Repositories/Repository.cs b/Todo.Infrastructure/Repositories/Repository.cs new file mode 100644 index 00000000..217b2cf8 --- /dev/null +++ b/Todo.Infrastructure/Repositories/Repository.cs @@ -0,0 +1,56 @@ +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore; +using Todo.Infrastructure.DbContexts; +using Todo.Infrastructure.Interfaces; + +namespace Todo.Infrastructure.Repositories; + +public class Repository : IRepository + where T : class +{ + private readonly TodoDbContext _context; + + public Repository(TodoDbContext context) + { + _context = context; + } + + public void Create(T entity) + { + _context.Set().Add(entity); + } + + public async Task Read(Expression> condition) + { + T item = await _context.Set() + .Where(condition) + .AsNoTracking() + .FirstOrDefaultAsync(); + + return item ?? Activator.CreateInstance(); // Return an empty instance if item is null + } + + public void Update(T entity) + { + _context.Set().Update(entity); + } + + public void Delete(T entity) + { + _context.Set().Remove(entity); + } + + public async Task SaveChanges() + { + await _context.SaveChangesAsync(); + } + + public async Task> GetAll() + { + List items = await _context.Set() + .AsNoTracking() + .ToListAsync(); + + return items; + } +} \ No newline at end of file diff --git a/Todo.Infrastructure/Todo.Infrastructure.csproj b/Todo.Infrastructure/Todo.Infrastructure.csproj new file mode 100644 index 00000000..7aea24f2 --- /dev/null +++ b/Todo.Infrastructure/Todo.Infrastructure.csproj @@ -0,0 +1,18 @@ + + + + net7.0 + enable + enable + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/Todo.Test/Todo.Test.csproj b/Todo.Test/Todo.Test.csproj new file mode 100644 index 00000000..3b886f13 --- /dev/null +++ b/Todo.Test/Todo.Test.csproj @@ -0,0 +1,26 @@ + + + + net7.0 + enable + enable + + false + + + + + + + + + + + + + + + + + + diff --git a/Todo.Test/TodoItemsTest.cs b/Todo.Test/TodoItemsTest.cs new file mode 100644 index 00000000..8c80f52b --- /dev/null +++ b/Todo.Test/TodoItemsTest.cs @@ -0,0 +1,117 @@ +using AutoMapper; +using Microsoft.AspNetCore.Mvc; +using Moq; +using Todo.API.Controllers; +using Todo.Core.Interfaces; +using Todo.Core.Models.TodoItem; +using Todo.Infrastructure.Entities; + +namespace Todo.Test; + +public class TodoItemsTest +{ + [TestFixture] + public class TodoItemsControllerTests + { + private readonly Mock mockMapper = new Mock(); + private readonly Mock mockTodoItemService = new Mock(); + + private readonly IReadOnlyList todoItems = + new List + { + new TodoItem { Id = 3, Name = "Iphone", IsComplete = false }, + new TodoItem { Id = 1, Name = "Samsung", IsComplete = true }, + new TodoItem { Id = 2, Name = "LG", IsComplete = false } + }; + + private TodoItemsController todoItemsController; + + [SetUp] + public void Setup() + { + // Configure AutoMapper mock + mockMapper.Setup(m => m.Map>(It.IsAny>())) + .Returns((List items) => items.Select(item => new TodoItemDTO())); + + mockMapper.Setup(m => m.Map(It.IsAny())) + .Returns((TodoItem item) => new TodoItemDTO()); + + // Configure TodoItemService mock + mockTodoItemService.Setup(service => service.GetAll()).ReturnsAsync(todoItems); + + mockTodoItemService.Setup(t => t.Read(1)).ReturnsAsync(todoItems[0]); + + todoItemsController = new TodoItemsController(mockTodoItemService.Object, mockMapper.Object); + } + + [Test] + public async Task GetTodoItems_ShouldReturnOkAndCallOnce_WhenTodoItemsExist() + { + // Act + var result = await todoItemsController.GetTodoItems(); + + // Assert + Assert.IsInstanceOf>>(result); + mockTodoItemService.Verify(mock => mock.GetAll(), Times.Once()); + } + + [Test] + public async Task ShouldReturnOkAndCallOnce_WhenTodoItemExists() + { + // Arrange + var todoItem = new TodoItem { Id = 13, Name = "Test Item", IsComplete = false }; + mockTodoItemService.Setup(t => t.Read(13)).ReturnsAsync(todoItem); + + // Act + var result = await todoItemsController.GetTodoItem(13); + + // Assert + Assert.IsInstanceOf(result.Result); + mockTodoItemService.Verify(mock => mock.Read(13), Times.Once()); + } + + [Test] + public async Task Create_Todo_Item_Should_Return_Created_At_Route_Result_With_Created_Todo_Item() + { + // Arrange + var mockCreationDto = new TodoItemDTOCreate() { Name = "Write unit tests", IsComplete = false }; + var mockEntity = new TodoItem { Id = 55, Name = "tests", IsComplete = false }; + + mockMapper.Setup(m => m.Map(mockCreationDto)).Returns(mockEntity); + mockTodoItemService.Setup(s => s.Create(mockEntity)).Returns(Task.CompletedTask); + + // Act + var result = await todoItemsController.CreateTodoItem(mockCreationDto); + + // Assert + Assert.IsInstanceOf(result.Result); // Use .Result here to access the ActionResult + } + + [Test] + public async Task UpdateTodoItem_ShouldReturnNoContentResult() + { + // Arrange + var updateItemDto = new TodoItemDTOUpdate() { Name = "Do workout", IsComplete = true }; + var updateEntity = new TodoItem { Id = 3, Name = "Do workout", IsComplete = true }; + + mockMapper.Setup(mapper => mapper.Map(updateItemDto, todoItems[0])).Returns(updateEntity); + mockTodoItemService.Setup(service => service.Update(updateEntity)).Returns(Task.CompletedTask); + + // Act + var result = await todoItemsController.UpdateTodoItem(3, updateItemDto); + + // Assert + Assert.IsInstanceOf(result); + } + + [Test] + public async Task DeleteTodoItem_ShouldReturnNoContentResult() + { + // Act + var result = await todoItemsController.DeleteTodoItem(3); + + // Assert + Assert.IsInstanceOf(result); + } + } +} \ No newline at end of file diff --git a/Todo.Test/Usings.cs b/Todo.Test/Usings.cs new file mode 100644 index 00000000..cefced49 --- /dev/null +++ b/Todo.Test/Usings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file 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..a7d4f6ec 100644 --- a/TodoApiDTO.sln +++ b/TodoApiDTO.sln @@ -3,7 +3,13 @@ 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}") = "Todo.API", "Todo.API\Todo.API.csproj", "{623124F9-F5BA-42DD-BC26-A1720774229C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Todo.Core", "Todo.Core\Todo.Core.csproj", "{C6089C27-0FEF-414E-957B-D1B72E5E1BBD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Todo.Infrastructure", "Todo.Infrastructure\Todo.Infrastructure.csproj", "{9A288DEC-FCDC-4639-B301-F018AC43CE06}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Todo.Test", "Todo.Test\Todo.Test.csproj", "{E8932F55-4A85-45E9-9CA6-EB122D3BB153}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -15,6 +21,18 @@ Global {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 + {C6089C27-0FEF-414E-957B-D1B72E5E1BBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C6089C27-0FEF-414E-957B-D1B72E5E1BBD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C6089C27-0FEF-414E-957B-D1B72E5E1BBD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C6089C27-0FEF-414E-957B-D1B72E5E1BBD}.Release|Any CPU.Build.0 = Release|Any CPU + {9A288DEC-FCDC-4639-B301-F018AC43CE06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9A288DEC-FCDC-4639-B301-F018AC43CE06}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9A288DEC-FCDC-4639-B301-F018AC43CE06}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9A288DEC-FCDC-4639-B301-F018AC43CE06}.Release|Any CPU.Build.0 = Release|Any CPU + {E8932F55-4A85-45E9-9CA6-EB122D3BB153}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E8932F55-4A85-45E9-9CA6-EB122D3BB153}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E8932F55-4A85-45E9-9CA6-EB122D3BB153}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E8932F55-4A85-45E9-9CA6-EB122D3BB153}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/appsettings.json b/appsettings.json deleted file mode 100644 index d9d9a9bf..00000000 --- a/appsettings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, - "AllowedHosts": "*" -}