From f9ea3d5a5f5f3cb42694682bde9aff4c43f77dfa Mon Sep 17 00:00:00 2001 From: Mikhail Prokopenko Date: Fri, 18 Aug 2023 18:16:58 +0400 Subject: [PATCH 1/4] Added swagger --- Startup.cs | 22 +++++++++++++++------- TodoApiDTO.csproj | 3 ++- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/Startup.cs b/Startup.cs index bbfbc83d..4c2e3dfd 100644 --- a/Startup.cs +++ b/Startup.cs @@ -1,16 +1,10 @@ -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 Microsoft.OpenApi.Models; using TodoApi.Models; namespace TodoApi @@ -30,6 +24,11 @@ public void ConfigureServices(IServiceCollection services) services.AddDbContext(opt => opt.UseInMemoryDatabase("TodoList")); services.AddControllers(); + + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo { Title = "Todo API", Version = "v1" }); + }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -50,6 +49,15 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { endpoints.MapControllers(); }); + + app.UseSwagger(); + + app.UseSwaggerUI(settings => + { + settings.SwaggerEndpoint("/swagger/v1/swagger.json", "v1"); + settings.RoutePrefix = string.Empty; + + }); } } } diff --git a/TodoApiDTO.csproj b/TodoApiDTO.csproj index bba6f6af..2e1ebc7a 100644 --- a/TodoApiDTO.csproj +++ b/TodoApiDTO.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net6.0 @@ -12,6 +12,7 @@ + From b06e5114f45a9dc5e1c85e3c51cfd79dc4b9a285 Mon Sep 17 00:00:00 2001 From: Mikhail Prokopenko Date: Fri, 18 Aug 2023 18:26:47 +0400 Subject: [PATCH 2/4] SQL Server used NuGet packages have been updated --- .../20230818142422_TodoItemApi_1.Designer.cs | 51 +++++++++++++++++++ Migrations/20230818142422_TodoItemApi_1.cs | 36 +++++++++++++ Migrations/TodoContextModelSnapshot.cs | 48 +++++++++++++++++ Startup.cs | 7 ++- TodoApiDTO.csproj | 14 +++-- appsettings.json | 5 +- 6 files changed, 153 insertions(+), 8 deletions(-) create mode 100644 Migrations/20230818142422_TodoItemApi_1.Designer.cs create mode 100644 Migrations/20230818142422_TodoItemApi_1.cs create mode 100644 Migrations/TodoContextModelSnapshot.cs diff --git a/Migrations/20230818142422_TodoItemApi_1.Designer.cs b/Migrations/20230818142422_TodoItemApi_1.Designer.cs new file mode 100644 index 00000000..45160ec2 --- /dev/null +++ b/Migrations/20230818142422_TodoItemApi_1.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 TodoApi.Models; + +#nullable disable + +namespace TodoApiDTO.Migrations +{ + [DbContext(typeof(TodoContext))] + [Migration("20230818142422_TodoItemApi_1")] + partial class TodoItemApi_1 + { + /// + 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("TodoApi.Models.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/Migrations/20230818142422_TodoItemApi_1.cs b/Migrations/20230818142422_TodoItemApi_1.cs new file mode 100644 index 00000000..b9ef4d6e --- /dev/null +++ b/Migrations/20230818142422_TodoItemApi_1.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TodoApiDTO.Migrations +{ + /// + public partial class TodoItemApi_1 : 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: true), + 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/Migrations/TodoContextModelSnapshot.cs b/Migrations/TodoContextModelSnapshot.cs new file mode 100644 index 00000000..83cf40c9 --- /dev/null +++ b/Migrations/TodoContextModelSnapshot.cs @@ -0,0 +1,48 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TodoApi.Models; + +#nullable disable + +namespace TodoApiDTO.Migrations +{ + [DbContext(typeof(TodoContext))] + partial class TodoContextModelSnapshot : 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("TodoApi.Models.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/Startup.cs b/Startup.cs index 4c2e3dfd..38fef17d 100644 --- a/Startup.cs +++ b/Startup.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.OpenApi.Models; +using System; using TodoApi.Models; namespace TodoApi @@ -21,8 +22,10 @@ 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.AddDbContext(options => + options.UseSqlServer(Configuration.GetConnectionString("DbConnection") ?? + throw new InvalidOperationException("Connection string 'DbConnection' not found."))); + services.AddControllers(); services.AddSwaggerGen(c => diff --git a/TodoApiDTO.csproj b/TodoApiDTO.csproj index 2e1ebc7a..f8c35483 100644 --- a/TodoApiDTO.csproj +++ b/TodoApiDTO.csproj @@ -1,17 +1,21 @@ - net6.0 + net7.0 - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/appsettings.json b/appsettings.json index d9d9a9bf..8d4d36b1 100644 --- a/appsettings.json +++ b/appsettings.json @@ -6,5 +6,8 @@ "Microsoft.Hosting.Lifetime": "Information" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "ConnectionStrings": { + "DbConnection": "Server=(localdb)\\MSSQLLocalDB; Database=TodoApp; Trusted_Connection=true; MultipleActiveResultSets=true; Integrated Security=true;" + } } From a1e81932daa256f1e5ff58e3c036e60f45b81111 Mon Sep 17 00:00:00 2001 From: Mikhail Prokopenko Date: Fri, 18 Aug 2023 18:31:03 +0400 Subject: [PATCH 3/4] Nlog added --- Logger/ExceptionHandler.cs | 66 ++++++++++++++++++++++++++++++++++++++ Program.cs | 28 ++++++++++++++-- Startup.cs | 3 ++ TodoApiDTO.csproj | 10 ++++++ nlog.config | 36 +++++++++++++++++++++ 5 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 Logger/ExceptionHandler.cs create mode 100644 nlog.config diff --git a/Logger/ExceptionHandler.cs b/Logger/ExceptionHandler.cs new file mode 100644 index 00000000..779aba10 --- /dev/null +++ b/Logger/ExceptionHandler.cs @@ -0,0 +1,66 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json; +using System.Net; +using System.Threading.Tasks; +using System; +using TodoApi.BLL.Infrastructure; +using NLog; + +namespace TodoApi.Logger +{ + public class ExceptionHandler + { + private readonly RequestDelegate _next; + + public ExceptionHandler(RequestDelegate next) + { + _next = next; + } + + public async Task Invoke(HttpContext context) + { + try + { + await _next(context); + } + catch (Exception ex) + { + await HandleExceptionAsync(context, ex); + } + } + + private static Task HandleExceptionAsync(HttpContext context, Exception exception) + { + HttpStatusCode status = HttpStatusCode.NotFound; + string stackTrace = exception.StackTrace; + string message = exception.Message; + + var isCustomError = false; + + var exceptionType = exception.GetType(); + + if (exceptionType == typeof(ValidationException)) + { + LogManager.GetLogger("CustomError").Error(exception + Environment.NewLine); + } + else if (exceptionType != typeof(DbUpdateConcurrencyException)) + { + status = exceptionType != typeof(DbUpdateConcurrencyException)? HttpStatusCode.InternalServerError: status; + LogManager.GetLogger("SystemError").Error(exception + Environment.NewLine); + } + + var exceptionResult = JsonConvert.SerializeObject( + new + { + error = message, + stackTrace + }); + + context.Response.ContentType = "application/json"; + context.Response.StatusCode = (int)status; + + return context.Response.WriteAsync(exceptionResult); + } + } +} diff --git a/Program.cs b/Program.cs index b27ac16a..a17468b4 100644 --- a/Program.cs +++ b/Program.cs @@ -6,6 +6,8 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using NLog; +using NLog.Web; namespace TodoApi { @@ -13,7 +15,24 @@ public class Program { public static void Main(string[] args) { - CreateHostBuilder(args).Build().Run(); + var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger(); + + try + { + logger.Debug("init main"); + CreateHostBuilder(args).Build().Run(); + } + catch (Exception exception) + { + //NLog: catch setup errors + logger.Error(exception, "Stopped program because of exception"); + throw; + } + finally + { + // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux) + NLog.LogManager.Shutdown(); + } } public static IHostBuilder CreateHostBuilder(string[] args) => @@ -21,6 +40,11 @@ public static IHostBuilder CreateHostBuilder(string[] args) => .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); - }); + }).ConfigureLogging(logging => + { + logging.ClearProviders(); + logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace); + }) + .UseNLog(); } } diff --git a/Startup.cs b/Startup.cs index 38fef17d..19c206dc 100644 --- a/Startup.cs +++ b/Startup.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.OpenApi.Models; using System; +using TodoApi.Logger; using TodoApi.Models; namespace TodoApi @@ -61,6 +62,8 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) settings.RoutePrefix = string.Empty; }); + + app.UseMiddleware(); } } } diff --git a/TodoApiDTO.csproj b/TodoApiDTO.csproj index f8c35483..3e0d9964 100644 --- a/TodoApiDTO.csproj +++ b/TodoApiDTO.csproj @@ -16,8 +16,18 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/nlog.config b/nlog.config new file mode 100644 index 00000000..541d03a5 --- /dev/null +++ b/nlog.config @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + From 4e8036fde016b1abf7d1785f106eb0b08908a87e Mon Sep 17 00:00:00 2001 From: Mikhail Prokopenko Date: Fri, 18 Aug 2023 19:00:13 +0400 Subject: [PATCH 4/4] Changed project name DAL project added BLL project added DTO project added --- Controllers/TodoItemsController.cs | 116 ------------------ .../Infrastructure/ValidationException.cs | 12 ++ TodoApi.BLL/Interfaces/ITodoItemService.cs | 14 +++ TodoApi.BLL/Services/TodoItemService.cs | 85 +++++++++++++ TodoApi.BLL/TodoApi - Backup.BLL.csproj | 14 +++ TodoApi.BLL/TodoApi.BLL.csproj | 20 +++ .../DBContext}/TodoContext.cs | 3 +- {Models => TodoApi.DAL/Entities}/TodoItem.cs | 5 +- TodoApi.DAL/Extensions/TodoItemExtension.cs | 16 +++ TodoApi.DAL/Interfaces/IRepository.cs | 12 ++ TodoApi.DAL/Interfaces/IUnitOfWork.cs | 10 ++ .../20230818145516_TodoItemDAL_1.Designer.cs | 12 +- .../20230818145516_TodoItemDAL_1.cs | 8 +- .../Migrations}/TodoContextModelSnapshot.cs | 8 +- TodoApi.DAL/Repositories/EFUnitOfWork.cs | 52 ++++++++ .../Repositories/TodoItemRepository.cs | 55 +++++++++ TodoApi.DAL/TodoApi.DAL.csproj | 31 +++++ TodoApi.DTO/TodoApi.DTO.csproj | 27 ++++ .../TodoItemDto.cs | 5 +- TodoApi/Controllers/TodoItemsController.cs | 55 +++++++++ .../Logger}/ExceptionHandler.cs | 2 - TodoApi/Models/TodoItem.cs | 18 +++ Program.cs => TodoApi/Program.cs | 0 .../Properties}/launchSettings.json | 0 README.md => TodoApi/README.md | 0 Startup.cs => TodoApi/Startup.cs | 2 +- TodoApiDTO.csproj => TodoApi/TodoApi.csproj | 4 + TodoApi/TodoApiDTO.sln | 25 ++++ .../appsettings.Development.json | 0 appsettings.json => TodoApi/appsettings.json | 0 nlog.config => TodoApi/nlog.config | 0 TodoApiDTO.sln | 32 +++-- 32 files changed, 498 insertions(+), 145 deletions(-) delete mode 100644 Controllers/TodoItemsController.cs create mode 100644 TodoApi.BLL/Infrastructure/ValidationException.cs create mode 100644 TodoApi.BLL/Interfaces/ITodoItemService.cs create mode 100644 TodoApi.BLL/Services/TodoItemService.cs create mode 100644 TodoApi.BLL/TodoApi - Backup.BLL.csproj create mode 100644 TodoApi.BLL/TodoApi.BLL.csproj rename {Models => TodoApi.DAL/DBContext}/TodoContext.cs (81%) rename {Models => TodoApi.DAL/Entities}/TodoItem.cs (76%) create mode 100644 TodoApi.DAL/Extensions/TodoItemExtension.cs create mode 100644 TodoApi.DAL/Interfaces/IRepository.cs create mode 100644 TodoApi.DAL/Interfaces/IUnitOfWork.cs rename Migrations/20230818142422_TodoItemApi_1.Designer.cs => TodoApi.DAL/Migrations/20230818145516_TodoItemDAL_1.Designer.cs (83%) rename Migrations/20230818142422_TodoItemApi_1.cs => TodoApi.DAL/Migrations/20230818145516_TodoItemDAL_1.cs (88%) rename {Migrations => TodoApi.DAL/Migrations}/TodoContextModelSnapshot.cs (87%) create mode 100644 TodoApi.DAL/Repositories/EFUnitOfWork.cs create mode 100644 TodoApi.DAL/Repositories/TodoItemRepository.cs create mode 100644 TodoApi.DAL/TodoApi.DAL.csproj create mode 100644 TodoApi.DTO/TodoApi.DTO.csproj rename Models/TodoItemDTO.cs => TodoApi.DTO/TodoItemDto.cs (72%) create mode 100644 TodoApi/Controllers/TodoItemsController.cs rename {Logger => TodoApi/Logger}/ExceptionHandler.cs (97%) create mode 100644 TodoApi/Models/TodoItem.cs rename Program.cs => TodoApi/Program.cs (100%) rename {Properties => TodoApi/Properties}/launchSettings.json (100%) rename README.md => TodoApi/README.md (100%) rename Startup.cs => TodoApi/Startup.cs (98%) rename TodoApiDTO.csproj => TodoApi/TodoApi.csproj (93%) create mode 100644 TodoApi/TodoApiDTO.sln rename appsettings.Development.json => TodoApi/appsettings.Development.json (100%) rename appsettings.json => TodoApi/appsettings.json (100%) rename nlog.config => TodoApi/nlog.config (100%) 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/TodoApi.BLL/Infrastructure/ValidationException.cs b/TodoApi.BLL/Infrastructure/ValidationException.cs new file mode 100644 index 00000000..088bbee2 --- /dev/null +++ b/TodoApi.BLL/Infrastructure/ValidationException.cs @@ -0,0 +1,12 @@ + +namespace TodoApi.BLL.Infrastructure +{ + public class ValidationException : Exception + { + public string Property { get; protected set; } + public ValidationException(string message, string prop) : base(message) + { + Property = prop; + } + } +} diff --git a/TodoApi.BLL/Interfaces/ITodoItemService.cs b/TodoApi.BLL/Interfaces/ITodoItemService.cs new file mode 100644 index 00000000..fd697ba7 --- /dev/null +++ b/TodoApi.BLL/Interfaces/ITodoItemService.cs @@ -0,0 +1,14 @@ +using TodoApi.DTO; + +namespace TodoApi.BLL.Interfaces +{ + public interface ITodoItemService + { + Task GetTodoItemAsync(long id); + Task> GetTodoItemsAsync(); + Task UpdateTodoItemAsync(long id, TodoItemDTO todoItemDTO); + Task CreateTodoItemAsync(TodoItemDTO todoItemDTO); + Task DeleteTodoItemAsync(long id); + void Dispose(); + } +} diff --git a/TodoApi.BLL/Services/TodoItemService.cs b/TodoApi.BLL/Services/TodoItemService.cs new file mode 100644 index 00000000..a37b57f9 --- /dev/null +++ b/TodoApi.BLL/Services/TodoItemService.cs @@ -0,0 +1,85 @@ +using TodoApi.BLL.Interfaces; +using TodoApi.DAL.Entities; +using TodoApi.DAL.Extensions; +using TodoApi.DAL.Interfaces; +using TodoApi.DTO; + +namespace TodoApi.BLL.Services +{ + public class TodoItemService : ITodoItemService + { + IUnitOfWork Database { get; set; } + + public TodoItemService(IUnitOfWork uow) + { + Database = uow; + } + + public async Task GetTodoItemAsync(long id) + { + var todoItem = await Database.TodoItems.GetTodoItemAsync(id); + + if (todoItem == null) + { + throw new KeyNotFoundException("Item not found!"); + } + return todoItem.ItemToDTO(); + } + + public async Task> GetTodoItemsAsync() + { + var todoItems = await Database.TodoItems.GetTodoItemsAsync(); + + return todoItems.Select(x => x.ItemToDTO()).ToList(); + } + + public async Task UpdateTodoItemAsync(long id, TodoItemDTO todoItemDTO) + { + if (id != todoItemDTO.Id) + { + throw new Exception("Different object IDs!"); + } + + var todoItem = await Database.TodoItems.GetTodoItemAsync(id); + if (todoItem == null) + { + throw new KeyNotFoundException("Item not found!"); + } + + todoItem.Name = todoItemDTO.Name; + todoItem.IsComplete = todoItemDTO.IsComplete; + + await Database.TodoItems.UpdateTodoItemAsync(todoItem); + } + + public async Task CreateTodoItemAsync(TodoItemDTO todoItemDTO) + { + + var todoItem = new TodoItem + { + Name = todoItemDTO.Name, + IsComplete = todoItemDTO.IsComplete, + Secret = string.Empty + }; + + return (await Database.TodoItems.CreateTodoItemAsync(todoItem)).ItemToDTO(); + } + + public async Task DeleteTodoItemAsync(long id) + { + var todoItem = await Database.TodoItems.GetTodoItemAsync(id); + + if (todoItem == null) + { + throw new KeyNotFoundException("Item not found!"); + } + + await Database.TodoItems.DeleteTodoItemAsync(id); + } + + public void Dispose() + { + Database.Dispose(); + } + } +} diff --git a/TodoApi.BLL/TodoApi - Backup.BLL.csproj b/TodoApi.BLL/TodoApi - Backup.BLL.csproj new file mode 100644 index 00000000..451ff2c6 --- /dev/null +++ b/TodoApi.BLL/TodoApi - Backup.BLL.csproj @@ -0,0 +1,14 @@ + + + + net7.0 + enable + enable + + + + + + + + diff --git a/TodoApi.BLL/TodoApi.BLL.csproj b/TodoApi.BLL/TodoApi.BLL.csproj new file mode 100644 index 00000000..c1f70f88 --- /dev/null +++ b/TodoApi.BLL/TodoApi.BLL.csproj @@ -0,0 +1,20 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + + + diff --git a/Models/TodoContext.cs b/TodoApi.DAL/DBContext/TodoContext.cs similarity index 81% rename from Models/TodoContext.cs rename to TodoApi.DAL/DBContext/TodoContext.cs index 6e59e363..05758eab 100644 --- a/Models/TodoContext.cs +++ b/TodoApi.DAL/DBContext/TodoContext.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; +using TodoApi.DAL.Entities; -namespace TodoApi.Models +namespace TodoApi.DAL.DBContext { public class TodoContext : DbContext { diff --git a/Models/TodoItem.cs b/TodoApi.DAL/Entities/TodoItem.cs similarity index 76% rename from Models/TodoItem.cs rename to TodoApi.DAL/Entities/TodoItem.cs index 1f6e5465..929c6748 100644 --- a/Models/TodoItem.cs +++ b/TodoApi.DAL/Entities/TodoItem.cs @@ -1,6 +1,6 @@ -namespace TodoApi.Models + +namespace TodoApi.DAL.Entities { - #region snippet public class TodoItem { public long Id { get; set; } @@ -8,5 +8,4 @@ public class TodoItem public bool IsComplete { get; set; } public string Secret { get; set; } } - #endregion } \ No newline at end of file diff --git a/TodoApi.DAL/Extensions/TodoItemExtension.cs b/TodoApi.DAL/Extensions/TodoItemExtension.cs new file mode 100644 index 00000000..4ad7f8f6 --- /dev/null +++ b/TodoApi.DAL/Extensions/TodoItemExtension.cs @@ -0,0 +1,16 @@ +using TodoApi.DTO; +using TodoApi.DAL.Entities; + +namespace TodoApi.DAL.Extensions +{ + public static class TodoItemExtension + { + public static TodoItemDTO ItemToDTO(this TodoItem todoItem) => + new TodoItemDTO + { + Id = todoItem.Id, + Name = todoItem.Name, + IsComplete = todoItem.IsComplete + }; + } +} diff --git a/TodoApi.DAL/Interfaces/IRepository.cs b/TodoApi.DAL/Interfaces/IRepository.cs new file mode 100644 index 00000000..f1f6a80b --- /dev/null +++ b/TodoApi.DAL/Interfaces/IRepository.cs @@ -0,0 +1,12 @@ + +namespace TodoApi.DAL.Interfaces +{ + public interface IRepository where T : class + { + Task GetTodoItemAsync(long id); + Task> GetTodoItemsAsync(); + Task UpdateTodoItemAsync(T todoItemDTO); + Task CreateTodoItemAsync(T todoItemDTO); + Task DeleteTodoItemAsync(long id); + } +} diff --git a/TodoApi.DAL/Interfaces/IUnitOfWork.cs b/TodoApi.DAL/Interfaces/IUnitOfWork.cs new file mode 100644 index 00000000..20577895 --- /dev/null +++ b/TodoApi.DAL/Interfaces/IUnitOfWork.cs @@ -0,0 +1,10 @@ + +using TodoApi.DAL.Entities; + +namespace TodoApi.DAL.Interfaces +{ + public interface IUnitOfWork : IDisposable + { + IRepository TodoItems { get; } + } +} diff --git a/Migrations/20230818142422_TodoItemApi_1.Designer.cs b/TodoApi.DAL/Migrations/20230818145516_TodoItemDAL_1.Designer.cs similarity index 83% rename from Migrations/20230818142422_TodoItemApi_1.Designer.cs rename to TodoApi.DAL/Migrations/20230818145516_TodoItemDAL_1.Designer.cs index 45160ec2..cff6dec2 100644 --- a/Migrations/20230818142422_TodoItemApi_1.Designer.cs +++ b/TodoApi.DAL/Migrations/20230818145516_TodoItemDAL_1.Designer.cs @@ -4,15 +4,15 @@ using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using TodoApi.Models; +using TodoApi.DAL.DBContext; #nullable disable -namespace TodoApiDTO.Migrations +namespace TodoApi.DAL.Migrations { [DbContext(typeof(TodoContext))] - [Migration("20230818142422_TodoItemApi_1")] - partial class TodoItemApi_1 + [Migration("20230818145516_TodoItemDAL_1")] + partial class TodoItemDAL_1 { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -24,7 +24,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - modelBuilder.Entity("TodoApi.Models.TodoItem", b => + modelBuilder.Entity("TodoApi.DAL.Entities.TodoItem", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -36,9 +36,11 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("bit"); b.Property("Name") + .IsRequired() .HasColumnType("nvarchar(max)"); b.Property("Secret") + .IsRequired() .HasColumnType("nvarchar(max)"); b.HasKey("Id"); diff --git a/Migrations/20230818142422_TodoItemApi_1.cs b/TodoApi.DAL/Migrations/20230818145516_TodoItemDAL_1.cs similarity index 88% rename from Migrations/20230818142422_TodoItemApi_1.cs rename to TodoApi.DAL/Migrations/20230818145516_TodoItemDAL_1.cs index b9ef4d6e..bf8cf104 100644 --- a/Migrations/20230818142422_TodoItemApi_1.cs +++ b/TodoApi.DAL/Migrations/20230818145516_TodoItemDAL_1.cs @@ -2,10 +2,10 @@ #nullable disable -namespace TodoApiDTO.Migrations +namespace TodoApi.DAL.Migrations { /// - public partial class TodoItemApi_1 : Migration + public partial class TodoItemDAL_1 : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) @@ -16,9 +16,9 @@ protected override void Up(MigrationBuilder migrationBuilder) { Id = table.Column(type: "bigint", nullable: false) .Annotation("SqlServer:Identity", "1, 1"), - Name = table.Column(type: "nvarchar(max)", nullable: true), + Name = table.Column(type: "nvarchar(max)", nullable: false), IsComplete = table.Column(type: "bit", nullable: false), - Secret = table.Column(type: "nvarchar(max)", nullable: true) + Secret = table.Column(type: "nvarchar(max)", nullable: false) }, constraints: table => { diff --git a/Migrations/TodoContextModelSnapshot.cs b/TodoApi.DAL/Migrations/TodoContextModelSnapshot.cs similarity index 87% rename from Migrations/TodoContextModelSnapshot.cs rename to TodoApi.DAL/Migrations/TodoContextModelSnapshot.cs index 83cf40c9..9f72e426 100644 --- a/Migrations/TodoContextModelSnapshot.cs +++ b/TodoApi.DAL/Migrations/TodoContextModelSnapshot.cs @@ -3,11 +3,11 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using TodoApi.Models; +using TodoApi.DAL.DBContext; #nullable disable -namespace TodoApiDTO.Migrations +namespace TodoApi.DAL.Migrations { [DbContext(typeof(TodoContext))] partial class TodoContextModelSnapshot : ModelSnapshot @@ -21,7 +21,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - modelBuilder.Entity("TodoApi.Models.TodoItem", b => + modelBuilder.Entity("TodoApi.DAL.Entities.TodoItem", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -33,9 +33,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("bit"); b.Property("Name") + .IsRequired() .HasColumnType("nvarchar(max)"); b.Property("Secret") + .IsRequired() .HasColumnType("nvarchar(max)"); b.HasKey("Id"); diff --git a/TodoApi.DAL/Repositories/EFUnitOfWork.cs b/TodoApi.DAL/Repositories/EFUnitOfWork.cs new file mode 100644 index 00000000..7e35a2c1 --- /dev/null +++ b/TodoApi.DAL/Repositories/EFUnitOfWork.cs @@ -0,0 +1,52 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TodoApi.DAL.DBContext; +using TodoApi.DAL.Entities; +using TodoApi.DAL.Interfaces; + +namespace TodoApi.DAL.Repositories +{ + public class EFUnitOfWork : IUnitOfWork + { + private TodoContext db; + private TodoItemRepository todoItemRepository; + + public EFUnitOfWork(DbContextOptions options) + { + db = new TodoContext(options); + } + public IRepository TodoItems + { + get + { + if (todoItemRepository == null) + todoItemRepository = new TodoItemRepository(db); + return todoItemRepository; + } + } + + private bool disposed = false; + + public virtual void Dispose(bool disposing) + { + if (!this.disposed) + { + if (disposing) + { + db.Dispose(); + } + this.disposed = true; + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} diff --git a/TodoApi.DAL/Repositories/TodoItemRepository.cs b/TodoApi.DAL/Repositories/TodoItemRepository.cs new file mode 100644 index 00000000..e3338018 --- /dev/null +++ b/TodoApi.DAL/Repositories/TodoItemRepository.cs @@ -0,0 +1,55 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TodoApi.DAL.DBContext; +using TodoApi.DAL.Entities; +using TodoApi.DAL.Interfaces; + +namespace TodoApi.DAL.Repositories +{ + public class TodoItemRepository : IRepository + { + private TodoContext db; + + public TodoItemRepository(TodoContext context) + { + this.db = context; + } + + public async Task> GetTodoItemsAsync() + { + return await db.TodoItems.ToListAsync(); + } + + public async Task GetTodoItemAsync(long id) + { + return await db.TodoItems.FirstOrDefaultAsync(x => x.Id == id); + } + + public async Task CreateTodoItemAsync(TodoItem todoItem) + { + db.TodoItems.Add(todoItem); + await db.SaveChangesAsync(); + + return todoItem; + } + + public async Task UpdateTodoItemAsync(TodoItem todoItem) + { + db.Entry(todoItem).State = EntityState.Modified; + + await db.SaveChangesAsync(); + } + + public async Task DeleteTodoItemAsync(long id) + { + TodoItem todoItem = await db.TodoItems.FindAsync(id); + if (todoItem != null) + db.TodoItems.Remove(todoItem); + await db.SaveChangesAsync(); + } + } +} diff --git a/TodoApi.DAL/TodoApi.DAL.csproj b/TodoApi.DAL/TodoApi.DAL.csproj new file mode 100644 index 00000000..90966ffe --- /dev/null +++ b/TodoApi.DAL/TodoApi.DAL.csproj @@ -0,0 +1,31 @@ + + + + net7.0 + enable + enable + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + diff --git a/TodoApi.DTO/TodoApi.DTO.csproj b/TodoApi.DTO/TodoApi.DTO.csproj new file mode 100644 index 00000000..293106a9 --- /dev/null +++ b/TodoApi.DTO/TodoApi.DTO.csproj @@ -0,0 +1,27 @@ + + + + net7.0 + enable + enable + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/Models/TodoItemDTO.cs b/TodoApi.DTO/TodoItemDto.cs similarity index 72% rename from Models/TodoItemDTO.cs rename to TodoApi.DTO/TodoItemDto.cs index e66a500a..acfdb09a 100644 --- a/Models/TodoItemDTO.cs +++ b/TodoApi.DTO/TodoItemDto.cs @@ -1,11 +1,10 @@ -namespace TodoApi.Models + +namespace TodoApi.DTO { - #region snippet public class TodoItemDTO { public long Id { get; set; } public string Name { get; set; } public bool IsComplete { get; set; } } - #endregion } diff --git a/TodoApi/Controllers/TodoItemsController.cs b/TodoApi/Controllers/TodoItemsController.cs new file mode 100644 index 00000000..8eab309c --- /dev/null +++ b/TodoApi/Controllers/TodoItemsController.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using TodoApi.BLL.Interfaces; +using TodoApi.DTO; + +namespace TodoApi.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class TodoItemsController : ControllerBase + { + + private readonly ITodoItemService _todoItemService; + + public TodoItemsController(ITodoItemService todoItemService) + { + _todoItemService = todoItemService; + } + + [HttpGet] + public async Task>> GetTodoItems() + { + return (await _todoItemService.GetTodoItemsAsync()).ToList(); + } + + [HttpGet("{id}")] + public async Task> GetTodoItem(long id) + { + return await _todoItemService.GetTodoItemAsync(id); + } + + [HttpPut("{id}")] + public async Task UpdateTodoItem(long id, TodoItemDTO todoItemDTO) + { + await _todoItemService.UpdateTodoItemAsync(id, todoItemDTO); + return Ok(); + } + + [HttpPost] + public async Task> CreateTodoItem(TodoItemDTO todoItemDTO) + { + return await _todoItemService.CreateTodoItemAsync(todoItemDTO); + } + + [HttpDelete("{id}")] + public async Task DeleteTodoItem(long id) + { + await _todoItemService.DeleteTodoItemAsync(id); + + return Ok(); + } + } +} diff --git a/Logger/ExceptionHandler.cs b/TodoApi/Logger/ExceptionHandler.cs similarity index 97% rename from Logger/ExceptionHandler.cs rename to TodoApi/Logger/ExceptionHandler.cs index 779aba10..be373163 100644 --- a/Logger/ExceptionHandler.cs +++ b/TodoApi/Logger/ExceptionHandler.cs @@ -36,8 +36,6 @@ private static Task HandleExceptionAsync(HttpContext context, Exception exceptio string stackTrace = exception.StackTrace; string message = exception.Message; - var isCustomError = false; - var exceptionType = exception.GetType(); if (exceptionType == typeof(ValidationException)) diff --git a/TodoApi/Models/TodoItem.cs b/TodoApi/Models/TodoItem.cs new file mode 100644 index 00000000..ca8650e1 --- /dev/null +++ b/TodoApi/Models/TodoItem.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace TodoApi.Models +{ + #region snippet + public class TodoItem + { + [Required] + public long Id { get; set; } + [Required] + [MaxLength(100)] + public string Name { get; set; } + [Required] + public bool IsComplete { get; set; } + public string Secret { get; set; } + } + #endregion +} \ No newline at end of file diff --git a/Program.cs b/TodoApi/Program.cs similarity index 100% rename from Program.cs rename to TodoApi/Program.cs diff --git a/Properties/launchSettings.json b/TodoApi/Properties/launchSettings.json similarity index 100% rename from Properties/launchSettings.json rename to TodoApi/Properties/launchSettings.json diff --git a/README.md b/TodoApi/README.md similarity index 100% rename from README.md rename to TodoApi/README.md diff --git a/Startup.cs b/TodoApi/Startup.cs similarity index 98% rename from Startup.cs rename to TodoApi/Startup.cs index 19c206dc..fa42f916 100644 --- a/Startup.cs +++ b/TodoApi/Startup.cs @@ -6,8 +6,8 @@ using Microsoft.Extensions.Hosting; using Microsoft.OpenApi.Models; using System; +using TodoApi.DAL.DBContext; using TodoApi.Logger; -using TodoApi.Models; namespace TodoApi { diff --git a/TodoApiDTO.csproj b/TodoApi/TodoApi.csproj similarity index 93% rename from TodoApiDTO.csproj rename to TodoApi/TodoApi.csproj index 3e0d9964..35254501 100644 --- a/TodoApiDTO.csproj +++ b/TodoApi/TodoApi.csproj @@ -25,6 +25,10 @@ + + + + diff --git a/TodoApi/TodoApiDTO.sln b/TodoApi/TodoApiDTO.sln new file mode 100644 index 00000000..e49c182b --- /dev/null +++ b/TodoApi/TodoApiDTO.sln @@ -0,0 +1,25 @@ + +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}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + 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 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {60984BC9-01D1-4B40-9733-E459B1231F96} + EndGlobalSection +EndGlobal 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 100% rename from appsettings.json rename to TodoApi/appsettings.json diff --git a/nlog.config b/TodoApi/nlog.config similarity index 100% rename from nlog.config rename to TodoApi/nlog.config diff --git a/TodoApiDTO.sln b/TodoApiDTO.sln index e49c182b..7537ebd4 100644 --- a/TodoApiDTO.sln +++ b/TodoApiDTO.sln @@ -1,9 +1,15 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30002.166 +# Visual Studio Version 17 +VisualStudioVersion = 17.7.34003.232 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.DTO", "TodoApi.DTO\TodoApi.DTO.csproj", "{EB567F3C-9932-4B18-B8AD-FC461ECA0A96}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApi.BLL", "TodoApi.BLL\TodoApi.BLL.csproj", "{0E77E7F2-BE6C-4A3E-A469-9471336A5053}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApi", "TodoApi\TodoApi.csproj", "{7D7BF2C5-FD8F-4FE8-9024-02082C03852C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApi.DAL", "TodoApi.DAL\TodoApi.DAL.csproj", "{501DEE26-ED46-4905-9FAA-9FA7FF84568B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -11,10 +17,22 @@ 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 + {EB567F3C-9932-4B18-B8AD-FC461ECA0A96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EB567F3C-9932-4B18-B8AD-FC461ECA0A96}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB567F3C-9932-4B18-B8AD-FC461ECA0A96}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EB567F3C-9932-4B18-B8AD-FC461ECA0A96}.Release|Any CPU.Build.0 = Release|Any CPU + {0E77E7F2-BE6C-4A3E-A469-9471336A5053}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0E77E7F2-BE6C-4A3E-A469-9471336A5053}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0E77E7F2-BE6C-4A3E-A469-9471336A5053}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0E77E7F2-BE6C-4A3E-A469-9471336A5053}.Release|Any CPU.Build.0 = Release|Any CPU + {7D7BF2C5-FD8F-4FE8-9024-02082C03852C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7D7BF2C5-FD8F-4FE8-9024-02082C03852C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7D7BF2C5-FD8F-4FE8-9024-02082C03852C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7D7BF2C5-FD8F-4FE8-9024-02082C03852C}.Release|Any CPU.Build.0 = Release|Any CPU + {501DEE26-ED46-4905-9FAA-9FA7FF84568B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {501DEE26-ED46-4905-9FAA-9FA7FF84568B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {501DEE26-ED46-4905-9FAA-9FA7FF84568B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {501DEE26-ED46-4905-9FAA-9FA7FF84568B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE