From a76785e6026c00a421875c94b65f8c53c24a0b52 Mon Sep 17 00:00:00 2001 From: rickcowan Date: Sat, 2 May 2026 19:20:21 -0500 Subject: [PATCH 1/2] Removed strongly typed inventory objects Added EAV inventory types Added EAV object handling philosophy Added controllers Added EF Core migration integration --- .../20260501023055_InitialCreate.Designer.cs | 182 ++++++++++++++++++ .../20260501023055_InitialCreate.cs | 136 +++++++++++++ .../Migrations/DBContextModelSnapshot.cs | 179 +++++++++++++++++ 3 files changed, 497 insertions(+) create mode 100644 WyvernInventory.Infrastructure/Migrations/20260501023055_InitialCreate.Designer.cs create mode 100644 WyvernInventory.Infrastructure/Migrations/20260501023055_InitialCreate.cs create mode 100644 WyvernInventory.Infrastructure/Migrations/DBContextModelSnapshot.cs diff --git a/WyvernInventory.Infrastructure/Migrations/20260501023055_InitialCreate.Designer.cs b/WyvernInventory.Infrastructure/Migrations/20260501023055_InitialCreate.Designer.cs new file mode 100644 index 0000000..650ad6b --- /dev/null +++ b/WyvernInventory.Infrastructure/Migrations/20260501023055_InitialCreate.Designer.cs @@ -0,0 +1,182 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using WyvernInventory.Infrastructure.Data; + +#nullable disable + +namespace WyvernInventory.Infrastructure.Migrations +{ + [DbContext(typeof(DBContext))] + [Migration("20260501023055_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("WyvernInventory.Core.Models.InventoryAttributeDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("DataType") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("TypeId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("TypeId"); + + b.ToTable("InventoryAttributeDefinitions"); + }); + + modelBuilder.Entity("WyvernInventory.Core.Models.InventoryAttributeValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AttributeDefinitionId") + .HasColumnType("int"); + + b.Property("BoolValue") + .HasColumnType("bit"); + + b.Property("DateTimeValue") + .HasColumnType("datetime2"); + + b.Property("DecimalValue") + .HasColumnType("decimal(18,2)"); + + b.Property("IntValue") + .HasColumnType("int"); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("StringValue") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AttributeDefinitionId"); + + b.HasIndex("ItemId", "AttributeDefinitionId") + .IsUnique(); + + b.ToTable("InventoryAttributeValues"); + }); + + modelBuilder.Entity("WyvernInventory.Core.Models.InventoryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TypeId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("TypeId"); + + b.ToTable("InventoryItems"); + }); + + modelBuilder.Entity("WyvernInventory.Core.Models.InventoryType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("InventoryTypes"); + }); + + modelBuilder.Entity("WyvernInventory.Core.Models.InventoryAttributeDefinition", b => + { + b.HasOne("WyvernInventory.Core.Models.InventoryType", "Type") + .WithMany("AttributeDefinitions") + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Type"); + }); + + modelBuilder.Entity("WyvernInventory.Core.Models.InventoryAttributeValue", b => + { + b.HasOne("WyvernInventory.Core.Models.InventoryAttributeDefinition", "AttributeDefinition") + .WithMany() + .HasForeignKey("AttributeDefinitionId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("WyvernInventory.Core.Models.InventoryItem", "Item") + .WithMany("AttributeValues") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AttributeDefinition"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("WyvernInventory.Core.Models.InventoryItem", b => + { + b.HasOne("WyvernInventory.Core.Models.InventoryType", "Type") + .WithMany() + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Type"); + }); + + modelBuilder.Entity("WyvernInventory.Core.Models.InventoryItem", b => + { + b.Navigation("AttributeValues"); + }); + + modelBuilder.Entity("WyvernInventory.Core.Models.InventoryType", b => + { + b.Navigation("AttributeDefinitions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/WyvernInventory.Infrastructure/Migrations/20260501023055_InitialCreate.cs b/WyvernInventory.Infrastructure/Migrations/20260501023055_InitialCreate.cs new file mode 100644 index 0000000..3cc990f --- /dev/null +++ b/WyvernInventory.Infrastructure/Migrations/20260501023055_InitialCreate.cs @@ -0,0 +1,136 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace WyvernInventory.Infrastructure.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "InventoryTypes", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_InventoryTypes", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "InventoryAttributeDefinitions", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + TypeId = table.Column(type: "int", nullable: true), + Name = table.Column(type: "nvarchar(max)", nullable: true), + DataType = table.Column(type: "int", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_InventoryAttributeDefinitions", x => x.Id); + table.ForeignKey( + name: "FK_InventoryAttributeDefinitions_InventoryTypes_TypeId", + column: x => x.TypeId, + principalTable: "InventoryTypes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "InventoryItems", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: false), + TypeId = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_InventoryItems", x => x.Id); + table.ForeignKey( + name: "FK_InventoryItems_InventoryTypes_TypeId", + column: x => x.TypeId, + principalTable: "InventoryTypes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "InventoryAttributeValues", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + ItemId = table.Column(type: "int", nullable: false), + AttributeDefinitionId = table.Column(type: "int", nullable: false), + StringValue = table.Column(type: "nvarchar(max)", nullable: true), + IntValue = table.Column(type: "int", nullable: true), + DecimalValue = table.Column(type: "decimal(18,2)", nullable: true), + BoolValue = table.Column(type: "bit", nullable: true), + DateTimeValue = table.Column(type: "datetime2", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_InventoryAttributeValues", x => x.Id); + table.ForeignKey( + name: "FK_InventoryAttributeValues_InventoryAttributeDefinitions_AttributeDefinitionId", + column: x => x.AttributeDefinitionId, + principalTable: "InventoryAttributeDefinitions", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_InventoryAttributeValues_InventoryItems_ItemId", + column: x => x.ItemId, + principalTable: "InventoryItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_InventoryAttributeDefinitions_TypeId", + table: "InventoryAttributeDefinitions", + column: "TypeId"); + + migrationBuilder.CreateIndex( + name: "IX_InventoryAttributeValues_AttributeDefinitionId", + table: "InventoryAttributeValues", + column: "AttributeDefinitionId"); + + migrationBuilder.CreateIndex( + name: "IX_InventoryAttributeValues_ItemId_AttributeDefinitionId", + table: "InventoryAttributeValues", + columns: new[] { "ItemId", "AttributeDefinitionId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_InventoryItems_TypeId", + table: "InventoryItems", + column: "TypeId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "InventoryAttributeValues"); + + migrationBuilder.DropTable( + name: "InventoryAttributeDefinitions"); + + migrationBuilder.DropTable( + name: "InventoryItems"); + + migrationBuilder.DropTable( + name: "InventoryTypes"); + } + } +} diff --git a/WyvernInventory.Infrastructure/Migrations/DBContextModelSnapshot.cs b/WyvernInventory.Infrastructure/Migrations/DBContextModelSnapshot.cs new file mode 100644 index 0000000..71f3310 --- /dev/null +++ b/WyvernInventory.Infrastructure/Migrations/DBContextModelSnapshot.cs @@ -0,0 +1,179 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using WyvernInventory.Infrastructure.Data; + +#nullable disable + +namespace WyvernInventory.Infrastructure.Migrations +{ + [DbContext(typeof(DBContext))] + partial class DBContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("WyvernInventory.Core.Models.InventoryAttributeDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("DataType") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("TypeId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("TypeId"); + + b.ToTable("InventoryAttributeDefinitions"); + }); + + modelBuilder.Entity("WyvernInventory.Core.Models.InventoryAttributeValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AttributeDefinitionId") + .HasColumnType("int"); + + b.Property("BoolValue") + .HasColumnType("bit"); + + b.Property("DateTimeValue") + .HasColumnType("datetime2"); + + b.Property("DecimalValue") + .HasColumnType("decimal(18,2)"); + + b.Property("IntValue") + .HasColumnType("int"); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("StringValue") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AttributeDefinitionId"); + + b.HasIndex("ItemId", "AttributeDefinitionId") + .IsUnique(); + + b.ToTable("InventoryAttributeValues"); + }); + + modelBuilder.Entity("WyvernInventory.Core.Models.InventoryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TypeId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("TypeId"); + + b.ToTable("InventoryItems"); + }); + + modelBuilder.Entity("WyvernInventory.Core.Models.InventoryType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("InventoryTypes"); + }); + + modelBuilder.Entity("WyvernInventory.Core.Models.InventoryAttributeDefinition", b => + { + b.HasOne("WyvernInventory.Core.Models.InventoryType", "Type") + .WithMany("AttributeDefinitions") + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Type"); + }); + + modelBuilder.Entity("WyvernInventory.Core.Models.InventoryAttributeValue", b => + { + b.HasOne("WyvernInventory.Core.Models.InventoryAttributeDefinition", "AttributeDefinition") + .WithMany() + .HasForeignKey("AttributeDefinitionId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("WyvernInventory.Core.Models.InventoryItem", "Item") + .WithMany("AttributeValues") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AttributeDefinition"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("WyvernInventory.Core.Models.InventoryItem", b => + { + b.HasOne("WyvernInventory.Core.Models.InventoryType", "Type") + .WithMany() + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Type"); + }); + + modelBuilder.Entity("WyvernInventory.Core.Models.InventoryItem", b => + { + b.Navigation("AttributeValues"); + }); + + modelBuilder.Entity("WyvernInventory.Core.Models.InventoryType", b => + { + b.Navigation("AttributeDefinitions"); + }); +#pragma warning restore 612, 618 + } + } +} -- 2.52.0 From 75c381e6454fc9ba4f53993f037b53f4b0132914 Mon Sep 17 00:00:00 2001 From: rickcowan Date: Sat, 2 May 2026 19:20:34 -0500 Subject: [PATCH 2/2] Removed strongly typed inventory objects Added EAV inventory types Added EAV object handling philosophy Added controllers Added EF Core migration integration --- .gitignore | 4 + .../Controllers/InventoryController.cs | 40 ------ .../Controllers/WeatherForecastController.cs | 37 ----- .../api/v1/InventoryItemController.cs | 66 +++++++++ .../api/v1/InventoryTypeController.cs | 67 +++++++++ WyvernInventory.API/Program.cs | 64 ++++++++- .../Properties/launchSettings.json | 2 +- WyvernInventory.API/WeatherForecast.cs | 12 -- .../WyvernInventory.API.csproj | 11 ++ WyvernInventory.API/WyvernInventory.API.http | 39 +++-- WyvernInventory.API/appsettings.json | 30 +++- .../Interfaces/Repos/IDbObjectRepo.cs | 6 +- .../Interfaces/Repos/IInventoryItemRepo.cs | 10 ++ .../Interfaces/Services/IDataObjectService.cs | 6 +- .../Services/IInventoryItemService.cs | 10 ++ WyvernInventory.Core/Models/Computer.cs | 6 - WyvernInventory.Core/Models/Cpu.cs | 6 - WyvernInventory.Core/Models/Enums.cs | 13 ++ WyvernInventory.Core/Models/Gpu.cs | 6 - .../Models/InventoryAttributeDefinition.cs | 10 ++ ....cs => InventoryAttributeDefinitionDto.cs} | 4 +- .../Models/InventoryAttributeValue.cs | 16 +++ .../Models/InventoryAttributeValueDto.cs | 16 +++ .../Models/InventoryAttributeValueRequest.cs | 15 ++ WyvernInventory.Core/Models/InventoryItem.cs | 10 ++ .../Models/InventoryItemDto.cs | 9 ++ .../Models/InventoryItemRequest.cs | 9 ++ WyvernInventory.Core/Models/InventoryType.cs | 8 ++ .../Models/InventoryTypeDto.cs | 8 ++ WyvernInventory.Core/Models/Router.cs | 6 - WyvernInventory.Core/Models/StorageDrive.cs | 6 - WyvernInventory.Core/Models/Switch.cs | 6 - .../WyvernInventory.Core.csproj | 13 ++ .../Data/DBContext.cs | 45 ++++++ .../Repos/GenericInventoryItemRepo.cs | 53 ------- .../Repos/InventoryItemRepo.cs | 133 ++++++++++++++++++ .../Repos/InventoryTypeRepo.cs | 86 +++++++++++ .../Services/GenericInventoryItemService.cs | 17 --- .../Services/InventoryItemService.cs | 16 +++ .../Services/InventoryTypeService.cs | 16 +++ .../Utils/DbUtils.cs | 22 +++ .../WyvernInventory.Infrastructure.csproj | 8 ++ WyvernInventory.sln | 1 + compose.yaml | 6 +- 44 files changed, 755 insertions(+), 219 deletions(-) delete mode 100644 WyvernInventory.API/Controllers/InventoryController.cs delete mode 100644 WyvernInventory.API/Controllers/WeatherForecastController.cs create mode 100644 WyvernInventory.API/Controllers/api/v1/InventoryItemController.cs create mode 100644 WyvernInventory.API/Controllers/api/v1/InventoryTypeController.cs delete mode 100644 WyvernInventory.API/WeatherForecast.cs create mode 100644 WyvernInventory.Core/Interfaces/Repos/IInventoryItemRepo.cs create mode 100644 WyvernInventory.Core/Interfaces/Services/IInventoryItemService.cs delete mode 100644 WyvernInventory.Core/Models/Computer.cs delete mode 100644 WyvernInventory.Core/Models/Cpu.cs create mode 100644 WyvernInventory.Core/Models/Enums.cs delete mode 100644 WyvernInventory.Core/Models/Gpu.cs create mode 100644 WyvernInventory.Core/Models/InventoryAttributeDefinition.cs rename WyvernInventory.Core/Models/{GenericInventoryItem.cs => InventoryAttributeDefinitionDto.cs} (54%) create mode 100644 WyvernInventory.Core/Models/InventoryAttributeValue.cs create mode 100644 WyvernInventory.Core/Models/InventoryAttributeValueDto.cs create mode 100644 WyvernInventory.Core/Models/InventoryAttributeValueRequest.cs create mode 100644 WyvernInventory.Core/Models/InventoryItem.cs create mode 100644 WyvernInventory.Core/Models/InventoryItemDto.cs create mode 100644 WyvernInventory.Core/Models/InventoryItemRequest.cs create mode 100644 WyvernInventory.Core/Models/InventoryType.cs create mode 100644 WyvernInventory.Core/Models/InventoryTypeDto.cs delete mode 100644 WyvernInventory.Core/Models/Router.cs delete mode 100644 WyvernInventory.Core/Models/StorageDrive.cs delete mode 100644 WyvernInventory.Core/Models/Switch.cs create mode 100644 WyvernInventory.Infrastructure/Data/DBContext.cs delete mode 100644 WyvernInventory.Infrastructure/Repos/GenericInventoryItemRepo.cs create mode 100644 WyvernInventory.Infrastructure/Repos/InventoryItemRepo.cs create mode 100644 WyvernInventory.Infrastructure/Repos/InventoryTypeRepo.cs delete mode 100644 WyvernInventory.Infrastructure/Services/GenericInventoryItemService.cs create mode 100644 WyvernInventory.Infrastructure/Services/InventoryItemService.cs create mode 100644 WyvernInventory.Infrastructure/Services/InventoryTypeService.cs create mode 100644 WyvernInventory.Infrastructure/Utils/DbUtils.cs diff --git a/.gitignore b/.gitignore index f42e905..5296309 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ .idea/**/dictionaries .idea/**/shelf +.idea/ + # AWS User-specific .idea/**/aws.xml @@ -479,3 +481,5 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml +# Docker +.env \ No newline at end of file diff --git a/WyvernInventory.API/Controllers/InventoryController.cs b/WyvernInventory.API/Controllers/InventoryController.cs deleted file mode 100644 index a9569f5..0000000 --- a/WyvernInventory.API/Controllers/InventoryController.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using WyvernInventory.Core.Interfaces.Services; -using WyvernInventory.Core.Models; - -namespace WyvernInventory.API.Controllers; - -[ApiController] -[Route("[controller]")] -public class InventoryController(IDataObjectService genericItemService) - : ControllerBase -{ - private readonly IDataObjectService _genericItemService = genericItemService; - - [HttpPost] - public List Get([FromBody] List items) - { - return _genericItemService.GetAsync(_ => _).Result; - } - - [HttpPost("upsert")] - public IResult Post([FromBody] List items) - { - (int, int) results = _genericItemService.UpsertAsync(items).Result; - - if (results.Item1 > 0 || results.Item2 > 0) - { - return Results.StatusCode(201); - } - - return Results.Ok(); - } - - [HttpDelete("{id}")] - public IResult Delete([FromRoute] int id) - { - _genericItemService.DeleteAsync(new() { new() { Id = id } }).Wait(); - - return Results.Ok(); - } -} \ No newline at end of file diff --git a/WyvernInventory.API/Controllers/WeatherForecastController.cs b/WyvernInventory.API/Controllers/WeatherForecastController.cs deleted file mode 100644 index 263c7f2..0000000 --- a/WyvernInventory.API/Controllers/WeatherForecastController.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace WyvernInventory.API.Controllers; - -[ApiController] -[Route("[controller]")] -public class WeatherForecastController : ControllerBase -{ - private static readonly string[] Summaries = - [ - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - ]; - - [HttpGet(Name = "GetWeatherForecast")] - public IEnumerable GetBwah() - { - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - TemperatureC = Random.Shared.Next(-20, 55), - Summary = Summaries[Random.Shared.Next(Summaries.Length)] - }) - .ToArray(); - } - - [HttpGet("bwah", Name = "GetWeatherForecastBwah")] - public IEnumerable Get() - { - return Enumerable.Range(1, 2).Select(index => new WeatherForecast - { - Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - TemperatureC = Random.Shared.Next(-20, 55), - Summary = Summaries[Random.Shared.Next(Summaries.Length)] - }) - .ToArray(); - } -} \ No newline at end of file diff --git a/WyvernInventory.API/Controllers/api/v1/InventoryItemController.cs b/WyvernInventory.API/Controllers/api/v1/InventoryItemController.cs new file mode 100644 index 0000000..1b31f33 --- /dev/null +++ b/WyvernInventory.API/Controllers/api/v1/InventoryItemController.cs @@ -0,0 +1,66 @@ +using Microsoft.AspNetCore.Mvc; +using Serilog.Events; +using WyvernInventory.Core.Interfaces.Services; +using WyvernInventory.Core.Models; +using ILogger = Serilog.ILogger; + +namespace WyvernInventory.API.Controllers.api.v1; + +[ApiController] +[Route("api/v1/[controller]")] +public class InventoryItemController(IInventoryItemService inventoryItemService, ILogger logger) : ControllerBase +{ + private readonly IInventoryItemService _inventoryItemService = inventoryItemService; + private ILogger _logger = logger; + + [HttpPost] + public ActionResult> Get([FromBody] List items) + { + try + { + return _inventoryItemService.GetAsync(items).Result; + } + catch (Exception e) + { + _logger.Write(LogEventLevel.Error, e, "An error occured"); + return StatusCode(500, "Internal server error"); + } + } + + [HttpPost("upsert")] + public ActionResult Post([FromBody] List items) + { + try + { + (int, int) results = _inventoryItemService.UpsertAsync(items).Result; + + if (results.Item1 > 0 || results.Item2 > 0) + { + return Ok(new { Created = results.Item1, Updated = results.Item2 }); + } + + return Ok(); + } + catch (Exception e) + { + _logger.Write(LogEventLevel.Error, e, "An error occured"); + return StatusCode(500, "Internal server error"); + } + } + + [HttpDelete("{id}")] + public ActionResult Delete([FromRoute] int id) + { + try + { + int results = _inventoryItemService.DeleteAsync([new() { Id = id }]).Result; + + return Ok(results); + } + catch (Exception e) + { + _logger.Write(LogEventLevel.Error, e, "An error occured"); + return StatusCode(500, "Internal server error"); + } + } +} \ No newline at end of file diff --git a/WyvernInventory.API/Controllers/api/v1/InventoryTypeController.cs b/WyvernInventory.API/Controllers/api/v1/InventoryTypeController.cs new file mode 100644 index 0000000..a359dab --- /dev/null +++ b/WyvernInventory.API/Controllers/api/v1/InventoryTypeController.cs @@ -0,0 +1,67 @@ +using Microsoft.AspNetCore.HttpLogging; +using Microsoft.AspNetCore.Mvc; +using Serilog.Events; +using WyvernInventory.Core.Interfaces.Services; +using WyvernInventory.Core.Models; +using ILogger = Serilog.ILogger; + +namespace WyvernInventory.API.Controllers.api.v1; + +[ApiController] +[Route("api/v1/[controller]")] +public class InventoryTypeController(IDataObjectService inventoryTypeService, ILogger logger) : ControllerBase +{ + private readonly IDataObjectService _inventoryTypeService = inventoryTypeService; + private ILogger _logger = logger; + + [HttpPost] + public ActionResult> Get([FromBody] List items) + { + try + { + return _inventoryTypeService.GetAsync(items).Result; + } + catch (Exception e) + { + _logger.Write(LogEventLevel.Error, e, "An error occured"); + return StatusCode(500, "Internal server error"); + } + } + + [HttpPost("upsert")] + public IActionResult Post([FromBody] List items) + { + try + { + (int, int) results = _inventoryTypeService.UpsertAsync(items).Result; + + if (results.Item1 > 0 || results.Item2 > 0) + { + return Ok(new { Created = results.Item1, Updated = results.Item2 }); + } + + return Ok(); + } + catch (Exception e) + { + _logger.Write(LogEventLevel.Error, e, "An error occured"); + return StatusCode(500, "Internal server error"); + } + } + + [HttpDelete("{id}")] + public IActionResult Delete([FromRoute] int id) + { + try + { + int results = _inventoryTypeService.DeleteAsync([new() { Id = id }]).Result; + + return Ok(results); + } + catch (Exception e) + { + _logger.Write(LogEventLevel.Error, e, "An error occured"); + return StatusCode(500, "Internal server error"); + } + } +} \ No newline at end of file diff --git a/WyvernInventory.API/Program.cs b/WyvernInventory.API/Program.cs index f6065fc..a4b4b03 100644 --- a/WyvernInventory.API/Program.cs +++ b/WyvernInventory.API/Program.cs @@ -1,27 +1,83 @@ +using System.Diagnostics; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using Serilog; +using Serilog.Events; using WyvernInventory.Core.Interfaces.Repos; using WyvernInventory.Core.Interfaces.Services; using WyvernInventory.Core.Models; +using WyvernInventory.Infrastructure.Data; using WyvernInventory.Infrastructure.Repos; using WyvernInventory.Infrastructure.Services; +using ILogger = Serilog.ILogger; var builder = WebApplication.CreateBuilder(args); -// Add services to the container. - builder.Services.AddControllers(); + +builder.Services.AddScoped, InventoryTypeService>(); +builder.Services.AddScoped, InventoryTypeRepo>(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); -builder.Services.AddSingleton, GenericInventoryItemRepo>(); -builder.Services.AddSingleton, GenericInventoryItemService>(); +builder.Services.AddSwaggerGen(); +builder.Host.UseSerilog((context, services, configuration) => + configuration + .ReadFrom.Configuration(context.Configuration) + .ReadFrom.Services(services) +); + +if (builder.Configuration["Database:ConnectionString"].IsNullOrEmpty()) +{ + ILogger logger = new LoggerConfiguration().ReadFrom.Configuration(builder.Configuration).CreateLogger(); + + logger.Fatal("Database environment variables not found"); + + Environment.Exit(1); +} +else +{ + builder.Services.AddDbContext(options => + { + options + .UseSqlServer(builder.Configuration["Database:ConnectionString"]).EnableSensitiveDataLogging(); + + if (builder.Environment.IsDevelopment()) + { + options + .EnableSensitiveDataLogging() + .EnableDetailedErrors() + .LogTo(msg => { Debug.WriteLine(msg); }, [DbLoggerCategory.Database.Command.Name], LogLevel.Information); + } + }); +} var app = builder.Build(); +using (var scope = app.Services.CreateScope()) +{ + DBContext db = scope.ServiceProvider.GetRequiredService(); + var logger = scope.ServiceProvider.GetRequiredService(); + + if (db.Database.GetPendingMigrations().Any()) + { + await db.Database.MigrateAsync().ConfigureAwait(false); + + logger.Write(LogEventLevel.Information, "Migrated database"); + } +} + // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.MapOpenApi(); } +app.UseSwagger(); +app.UseSwaggerUI(); + app.UseHttpsRedirection(); app.UseAuthorization(); diff --git a/WyvernInventory.API/Properties/launchSettings.json b/WyvernInventory.API/Properties/launchSettings.json index 2a3e901..60076f3 100644 --- a/WyvernInventory.API/Properties/launchSettings.json +++ b/WyvernInventory.API/Properties/launchSettings.json @@ -14,7 +14,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, - "applicationUrl": "https://localhost:7048;http://localhost:5244", + "applicationUrl": "https://0.0.0.0:7048;http://0.0.0.0:5244", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/WyvernInventory.API/WeatherForecast.cs b/WyvernInventory.API/WeatherForecast.cs deleted file mode 100644 index 15ccecf..0000000 --- a/WyvernInventory.API/WeatherForecast.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace WyvernInventory.API; - -public class WeatherForecast -{ - public DateOnly Date { get; set; } - - public int TemperatureC { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - - public string? Summary { get; set; } -} \ No newline at end of file diff --git a/WyvernInventory.API/WyvernInventory.API.csproj b/WyvernInventory.API/WyvernInventory.API.csproj index c010abe..3fb6c2a 100644 --- a/WyvernInventory.API/WyvernInventory.API.csproj +++ b/WyvernInventory.API/WyvernInventory.API.csproj @@ -9,6 +9,17 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/WyvernInventory.API/WyvernInventory.API.http b/WyvernInventory.API/WyvernInventory.API.http index 7880a67..5b2dc60 100644 --- a/WyvernInventory.API/WyvernInventory.API.http +++ b/WyvernInventory.API/WyvernInventory.API.http @@ -5,21 +5,42 @@ Content-Type: application/json [ { - "name": "RTX 4070" + "name": "RTX 4070 Ti Super" } ] ### -POST http://localhost:5244/inventory/upsert +POST http://localhost:5244/api/v1/InventoryType +Content-Type: application/json + +[ +] +### + +POST http://localhost:5244/api/v1/inventorytype/upsert Content-Type: application/json [ { - "name": "RTX 4070 Ti Super", - "description": "Desktop GPU" - }, - { - "name": "Ryzen 9 7950X3D", - "description": "Desktop CPU 16c/32t" + "name": "GPU", + "attributedefinitions": [ + { + "name": "Brand", + "dataType": 0 + }, + { + "name": "VRAM_GB", + "dataType": 1 + }, + { + "name": "Chipset", + "dataType": 0 + } + ] } -] \ No newline at end of file +] +### + +DELETE http://localhost:5244/api/v1/InventoryType/6 + +### \ No newline at end of file diff --git a/WyvernInventory.API/appsettings.json b/WyvernInventory.API/appsettings.json index 10f68b8..f4150f4 100644 --- a/WyvernInventory.API/appsettings.json +++ b/WyvernInventory.API/appsettings.json @@ -5,5 +5,33 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "Serilog": { + "Using": [ + "Serilog.Sinks.Console" + ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "System": "Warning", + "Microsoft.EntityFrameworkCore": "Warning" + } + }, + "Enrich": [ + "FromLogContext" + ], + "WriteTo": [ + { + "Name": "Console", + "Args": { + "outputTemplate": "[{Timestamp:MM-dd-yyyy HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}" + } + } + ] + }, + "Database": { + "ConnectionString": "" + } } diff --git a/WyvernInventory.Core/Interfaces/Repos/IDbObjectRepo.cs b/WyvernInventory.Core/Interfaces/Repos/IDbObjectRepo.cs index ae56c81..edcb7fd 100644 --- a/WyvernInventory.Core/Interfaces/Repos/IDbObjectRepo.cs +++ b/WyvernInventory.Core/Interfaces/Repos/IDbObjectRepo.cs @@ -1,8 +1,8 @@ namespace WyvernInventory.Core.Interfaces.Repos; -public interface IDbObjectRepo +public interface IDbObjectRepo { - public Task> GetAsync(Predicate? filter = null); + public Task> GetAsync(List? filterList = null); public Task<(int created, int updated)> UpsertAsync(List items); - public Task DeleteAsync(List items); + public Task DeleteAsync(List items); } \ No newline at end of file diff --git a/WyvernInventory.Core/Interfaces/Repos/IInventoryItemRepo.cs b/WyvernInventory.Core/Interfaces/Repos/IInventoryItemRepo.cs new file mode 100644 index 0000000..1314c81 --- /dev/null +++ b/WyvernInventory.Core/Interfaces/Repos/IInventoryItemRepo.cs @@ -0,0 +1,10 @@ +using WyvernInventory.Core.Models; + +namespace WyvernInventory.Core.Interfaces.Repos; + +public interface IInventoryItemRepo +{ + public Task> GetAsync(List? filterList = null); + public Task<(int created, int updated)> UpsertAsync(List items); + public Task DeleteAsync(List items); +} \ No newline at end of file diff --git a/WyvernInventory.Core/Interfaces/Services/IDataObjectService.cs b/WyvernInventory.Core/Interfaces/Services/IDataObjectService.cs index 770338f..643c70c 100644 --- a/WyvernInventory.Core/Interfaces/Services/IDataObjectService.cs +++ b/WyvernInventory.Core/Interfaces/Services/IDataObjectService.cs @@ -1,8 +1,8 @@ namespace WyvernInventory.Core.Interfaces.Services; -public interface IDataObjectService +public interface IDataObjectService { - public Task> GetAsync(Predicate? filter = null); + public Task> GetAsync(List? filter = null); public Task<(int created, int updated)> UpsertAsync(List items); - public Task DeleteAsync(List items); + public Task DeleteAsync(List items); } \ No newline at end of file diff --git a/WyvernInventory.Core/Interfaces/Services/IInventoryItemService.cs b/WyvernInventory.Core/Interfaces/Services/IInventoryItemService.cs new file mode 100644 index 0000000..009c59b --- /dev/null +++ b/WyvernInventory.Core/Interfaces/Services/IInventoryItemService.cs @@ -0,0 +1,10 @@ +using WyvernInventory.Core.Models; + +namespace WyvernInventory.Core.Interfaces.Services; + +public interface IInventoryItemService +{ + public Task> GetAsync(List? filter = null); + public Task<(int created, int updated)> UpsertAsync(List items); + public Task DeleteAsync(List items); +} \ No newline at end of file diff --git a/WyvernInventory.Core/Models/Computer.cs b/WyvernInventory.Core/Models/Computer.cs deleted file mode 100644 index d2d9d3e..0000000 --- a/WyvernInventory.Core/Models/Computer.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace WyvernInventory.Core.Models; - -public record Computer() : GenericInventoryItem -{ - -} \ No newline at end of file diff --git a/WyvernInventory.Core/Models/Cpu.cs b/WyvernInventory.Core/Models/Cpu.cs deleted file mode 100644 index 80ff5d4..0000000 --- a/WyvernInventory.Core/Models/Cpu.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace WyvernInventory.Core.Models; - -public record Cpu() : GenericInventoryItem -{ - -} \ No newline at end of file diff --git a/WyvernInventory.Core/Models/Enums.cs b/WyvernInventory.Core/Models/Enums.cs new file mode 100644 index 0000000..82ee76e --- /dev/null +++ b/WyvernInventory.Core/Models/Enums.cs @@ -0,0 +1,13 @@ +namespace WyvernInventory.Core.Models; + +public class Enums +{ + public enum DataType + { + String, + Int, + Decimal, + Bool, + DateTime + } +} \ No newline at end of file diff --git a/WyvernInventory.Core/Models/Gpu.cs b/WyvernInventory.Core/Models/Gpu.cs deleted file mode 100644 index 6596505..0000000 --- a/WyvernInventory.Core/Models/Gpu.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace WyvernInventory.Core.Models; - -public record Gpu() : GenericInventoryItem -{ - -} \ No newline at end of file diff --git a/WyvernInventory.Core/Models/InventoryAttributeDefinition.cs b/WyvernInventory.Core/Models/InventoryAttributeDefinition.cs new file mode 100644 index 0000000..d011143 --- /dev/null +++ b/WyvernInventory.Core/Models/InventoryAttributeDefinition.cs @@ -0,0 +1,10 @@ +namespace WyvernInventory.Core.Models; + +public record InventoryAttributeDefinition +{ + public int? Id { get; set; } + public int? TypeId { get; set; } + public InventoryType? Type { get; set; } = null!; + public string? Name { get; set; } = ""; + public Enums.DataType? DataType { get; set; } +} \ No newline at end of file diff --git a/WyvernInventory.Core/Models/GenericInventoryItem.cs b/WyvernInventory.Core/Models/InventoryAttributeDefinitionDto.cs similarity index 54% rename from WyvernInventory.Core/Models/GenericInventoryItem.cs rename to WyvernInventory.Core/Models/InventoryAttributeDefinitionDto.cs index a4126c0..c77771a 100644 --- a/WyvernInventory.Core/Models/GenericInventoryItem.cs +++ b/WyvernInventory.Core/Models/InventoryAttributeDefinitionDto.cs @@ -1,8 +1,8 @@ namespace WyvernInventory.Core.Models; -public record GenericInventoryItem() +public record InventoryAttributeDefinitionDto { public int? Id { get; set; } public string? Name { get; set; } - public string? Description { get; set; } + public Enums.DataType? DataType { get; set; } } \ No newline at end of file diff --git a/WyvernInventory.Core/Models/InventoryAttributeValue.cs b/WyvernInventory.Core/Models/InventoryAttributeValue.cs new file mode 100644 index 0000000..19cf497 --- /dev/null +++ b/WyvernInventory.Core/Models/InventoryAttributeValue.cs @@ -0,0 +1,16 @@ +namespace WyvernInventory.Core.Models; + +public record InventoryAttributeValue +{ + public int? Id { get; set; } + public int? ItemId { get; set; } + public InventoryItem? Item { get; set; } = null!; + public int? AttributeDefinitionId { get; set; } + public InventoryAttributeDefinition? AttributeDefinition { get; set; } = null!; + + public string? StringValue { get; set; } + public int? IntValue { get; set; } + public decimal? DecimalValue { get; set; } + public bool? BoolValue { get; set; } + public DateTime? DateTimeValue { get; set; } +} \ No newline at end of file diff --git a/WyvernInventory.Core/Models/InventoryAttributeValueDto.cs b/WyvernInventory.Core/Models/InventoryAttributeValueDto.cs new file mode 100644 index 0000000..e4d7be1 --- /dev/null +++ b/WyvernInventory.Core/Models/InventoryAttributeValueDto.cs @@ -0,0 +1,16 @@ +namespace WyvernInventory.Core.Models; + +public record InventoryAttributeValueDto +{ + public int? Id { get; set; } + public int? ItemId { get; set; } + public InventoryItemDto? Item { get; set; } = null!; + public int? AttributeDefinitionId { get; set; } + public InventoryAttributeDefinitionDto? AttributeDefinition { get; set; } = null!; + + public string? StringValue { get; set; } + public int? IntValue { get; set; } + public decimal? DecimalValue { get; set; } + public bool? BoolValue { get; set; } + public DateTime? DateTimeValue { get; set; } +} \ No newline at end of file diff --git a/WyvernInventory.Core/Models/InventoryAttributeValueRequest.cs b/WyvernInventory.Core/Models/InventoryAttributeValueRequest.cs new file mode 100644 index 0000000..6154bf8 --- /dev/null +++ b/WyvernInventory.Core/Models/InventoryAttributeValueRequest.cs @@ -0,0 +1,15 @@ +namespace WyvernInventory.Core.Models; + +public record InventoryAttributeValueRequest +{ + + public int? Id { get; set; } + public int? ItemId { get; set; } + public int? AttributeDefinitionId { get; set; } + + public string? StringValue { get; set; } + public int? IntValue { get; set; } + public decimal? DecimalValue { get; set; } + public bool? BoolValue { get; set; } + public DateTime? DateTimeValue { get; set; } +} \ No newline at end of file diff --git a/WyvernInventory.Core/Models/InventoryItem.cs b/WyvernInventory.Core/Models/InventoryItem.cs new file mode 100644 index 0000000..5eb7198 --- /dev/null +++ b/WyvernInventory.Core/Models/InventoryItem.cs @@ -0,0 +1,10 @@ +namespace WyvernInventory.Core.Models; + +public record InventoryItem +{ + public int? Id { get; set; } + public string? Name { get; set; } = ""; + public int? TypeId { get; set; } + public InventoryType? Type { get; set; } = null!; + public List? AttributeValues { get; set; } = []; +} \ No newline at end of file diff --git a/WyvernInventory.Core/Models/InventoryItemDto.cs b/WyvernInventory.Core/Models/InventoryItemDto.cs new file mode 100644 index 0000000..5692ef9 --- /dev/null +++ b/WyvernInventory.Core/Models/InventoryItemDto.cs @@ -0,0 +1,9 @@ +namespace WyvernInventory.Core.Models; + +public record InventoryItemDto +{ + public int Id { get; set; } + public string Name { get; set; } = ""; + public InventoryTypeDto Type { get; set; } + public List AttributeValues { get; set; } = []; +} \ No newline at end of file diff --git a/WyvernInventory.Core/Models/InventoryItemRequest.cs b/WyvernInventory.Core/Models/InventoryItemRequest.cs new file mode 100644 index 0000000..5e7817e --- /dev/null +++ b/WyvernInventory.Core/Models/InventoryItemRequest.cs @@ -0,0 +1,9 @@ +namespace WyvernInventory.Core.Models; + +public record InventoryItemRequest +{ + public int? Id { get; set; } + public string? Name { get; set; } = ""; + public int? TypeId { get; set; } + public List? AttributeValues { get; set; } = []; +} \ No newline at end of file diff --git a/WyvernInventory.Core/Models/InventoryType.cs b/WyvernInventory.Core/Models/InventoryType.cs new file mode 100644 index 0000000..eb5f93b --- /dev/null +++ b/WyvernInventory.Core/Models/InventoryType.cs @@ -0,0 +1,8 @@ +namespace WyvernInventory.Core.Models; + +public record InventoryType +{ + public int? Id { get; set; } + public string Name { get; set; } = ""; + public List AttributeDefinitions { get; set; } = []; +} \ No newline at end of file diff --git a/WyvernInventory.Core/Models/InventoryTypeDto.cs b/WyvernInventory.Core/Models/InventoryTypeDto.cs new file mode 100644 index 0000000..c632b10 --- /dev/null +++ b/WyvernInventory.Core/Models/InventoryTypeDto.cs @@ -0,0 +1,8 @@ +namespace WyvernInventory.Core.Models; + +public record InventoryTypeDto +{ + public int? Id { get; set; } + public string? Name { get; set; } + public List? AttributeDefinitions { get; set; } +} \ No newline at end of file diff --git a/WyvernInventory.Core/Models/Router.cs b/WyvernInventory.Core/Models/Router.cs deleted file mode 100644 index 3bd709c..0000000 --- a/WyvernInventory.Core/Models/Router.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace WyvernInventory.Core.Models; - -public record Router() : GenericInventoryItem -{ - -} \ No newline at end of file diff --git a/WyvernInventory.Core/Models/StorageDrive.cs b/WyvernInventory.Core/Models/StorageDrive.cs deleted file mode 100644 index abb210a..0000000 --- a/WyvernInventory.Core/Models/StorageDrive.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace WyvernInventory.Core.Models; - -public record StorageDrive() : GenericInventoryItem -{ - -} \ No newline at end of file diff --git a/WyvernInventory.Core/Models/Switch.cs b/WyvernInventory.Core/Models/Switch.cs deleted file mode 100644 index e7f7af6..0000000 --- a/WyvernInventory.Core/Models/Switch.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace WyvernInventory.Core.Models; - -public record Switch() : GenericInventoryItem -{ - -} \ No newline at end of file diff --git a/WyvernInventory.Core/WyvernInventory.Core.csproj b/WyvernInventory.Core/WyvernInventory.Core.csproj index 81e5926..b96413d 100644 --- a/WyvernInventory.Core/WyvernInventory.Core.csproj +++ b/WyvernInventory.Core/WyvernInventory.Core.csproj @@ -8,4 +8,17 @@ WyvernInventory.Core + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/WyvernInventory.Infrastructure/Data/DBContext.cs b/WyvernInventory.Infrastructure/Data/DBContext.cs new file mode 100644 index 0000000..054c423 --- /dev/null +++ b/WyvernInventory.Infrastructure/Data/DBContext.cs @@ -0,0 +1,45 @@ +using Microsoft.EntityFrameworkCore; +using WyvernInventory.Core.Models; + +namespace WyvernInventory.Infrastructure.Data; + +public class DBContext : DbContext +{ + + public DBContext(DbContextOptions options) : base(options) {} + + public DbSet InventoryItems => Set(); + public DbSet InventoryAttributeDefinitions => Set(); + public DbSet InventoryAttributeValues => Set(); + public DbSet InventoryTypes => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasOne(_ => _.Type) + .WithMany() + .HasForeignKey(_ => _.TypeId); + + modelBuilder.Entity() + .HasOne(_ => _.Item) + .WithMany(_ => _.AttributeValues) + .HasForeignKey(_ => _.ItemId) + .OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity() + .HasOne(_ => _.AttributeDefinition) + .WithMany() + .HasForeignKey(_ => _.AttributeDefinitionId) + .OnDelete(DeleteBehavior.NoAction); + + modelBuilder.Entity() + .HasOne(_ => _.Type) + .WithMany(_ => _.AttributeDefinitions) + .HasForeignKey(_ => _.TypeId) + .OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity() + .HasIndex(_ => new { _.ItemId, _.AttributeDefinitionId}) + .IsUnique(); + } +} \ No newline at end of file diff --git a/WyvernInventory.Infrastructure/Repos/GenericInventoryItemRepo.cs b/WyvernInventory.Infrastructure/Repos/GenericInventoryItemRepo.cs deleted file mode 100644 index 428182a..0000000 --- a/WyvernInventory.Infrastructure/Repos/GenericInventoryItemRepo.cs +++ /dev/null @@ -1,53 +0,0 @@ -using WyvernInventory.Core.Interfaces.Repos; -using WyvernInventory.Core.Models; - -namespace WyvernInventory.Infrastructure.Repos; - -public class GenericInventoryItemRepo : IDbObjectRepo -{ - private List _genericItemList = []; - - public async Task> GetAsync(Predicate? filter = null) - { - List result = []; - - if (filter is null) return _genericItemList; - - result.AddRange(_genericItemList.Where(item => filter(item))); - - return result; - } - - public async Task<(int created, int updated)> UpsertAsync(List items) - { - int created = 0; - int updated = 0; - foreach (var item in items) - { - if (item.Id is not null && _genericItemList.Any(_ => _.Id == item.Id)) - { - int index = _genericItemList.IndexOf(_genericItemList.Find(_ => _.Id == item.Id)); - _genericItemList[index] = item; - - updated++; - } - else - { - item.Id = _genericItemList.Count; - _genericItemList.Add(item); - - created++; - } - } - - return (created, updated); - } - - public async Task DeleteAsync(List items) - { - foreach (var item in items) - { - _genericItemList.Remove(_genericItemList.Find(_ => _.Id == item.Id)); - } - } -} \ No newline at end of file diff --git a/WyvernInventory.Infrastructure/Repos/InventoryItemRepo.cs b/WyvernInventory.Infrastructure/Repos/InventoryItemRepo.cs new file mode 100644 index 0000000..e12e97b --- /dev/null +++ b/WyvernInventory.Infrastructure/Repos/InventoryItemRepo.cs @@ -0,0 +1,133 @@ +using Microsoft.EntityFrameworkCore; +using WyvernInventory.Core.Interfaces.Repos; +using WyvernInventory.Core.Models; +using WyvernInventory.Infrastructure.Data; + +namespace WyvernInventory.Infrastructure.Repos; + +public class InventoryItemRepo(DBContext dbContext) : IInventoryItemRepo +{ + private DBContext _dbContext = dbContext; + + public async Task> GetAsync(List? filterList = null) + { + return _dbContext.InventoryItems + .Include(_ => _.AttributeValues) + .Include(_ => _.Type) + .Select(_ => new InventoryItemDto + { + Id = (int)_.Id!, + Name = _.Name, + AttributeValues = _.AttributeValues.Select(x => new InventoryAttributeValueDto + { + Id = x.Id, + AttributeDefinitionId = (int)x.AttributeDefinitionId!, + AttributeDefinition = new InventoryAttributeDefinitionDto() + { + Id = x.AttributeDefinitionId, + Name = x.AttributeDefinition.Name, + DataType = x.AttributeDefinition.DataType + }, + StringValue = x.StringValue, + IntValue = x.IntValue, + DecimalValue = x.DecimalValue, + BoolValue = x.BoolValue, + DateTimeValue = x.DateTimeValue + }).ToList(), + Type = new InventoryTypeDto() + { + Id = _.TypeId, + Name = _.Type.Name, + } + }) + .ToList(); + } + + public async Task<(int created, int updated)> UpsertAsync(List items) + { + var created = 0; + var updated = 0; + + var itemsToCreate = items + .Where(_ => _.Id is null or 0) + .Select(_ => new InventoryItem + { + Name = _.Name, + AttributeValues = _.AttributeValues?.Select(x => new InventoryAttributeValue + { + Id = x.Id, + AttributeDefinitionId = x.AttributeDefinitionId, + BoolValue = x.BoolValue, + DateTimeValue = x.DateTimeValue, + DecimalValue = x.DecimalValue, + IntValue = x.IntValue, + StringValue = x.StringValue + }).ToList(), + TypeId = _.TypeId + }) + .ToList(); + + var itemsToUpdate = items + .Where(_ => _.Id is not null and not 0) + .Select(_ => new InventoryItem + { + Id = _.Id, + Name = _.Name, + AttributeValues = _.AttributeValues?.Select(x => new InventoryAttributeValue + { + Id = x.Id, + AttributeDefinitionId = x.AttributeDefinitionId, + BoolValue = x.BoolValue, + DateTimeValue = x.DateTimeValue, + DecimalValue = x.DecimalValue, + IntValue = x.IntValue, + StringValue = x.StringValue + }).ToList(), + TypeId = _.TypeId + }) + .ToList(); + + if (itemsToCreate.Count > 0) + { + _dbContext.InventoryItems.AddRange(itemsToCreate); + created = itemsToCreate.Count; + } + + if (itemsToUpdate.Count > 0) + { + List idList = itemsToUpdate.Select(_ => + { + if (_.Id != null) return (int)_.Id; + throw new NullReferenceException(); + }).ToList(); + + var existingItems = _dbContext.InventoryItems.ToList() + .Where(_ => idList.Contains((int)_.Id!)) + .ToDictionary(_ => (int)_.Id!); + + foreach (var incoming in itemsToUpdate) + { + if (!existingItems.TryGetValue(incoming.Id!.Value, out var existing)) + continue; + + existing.Name = incoming.Name; + existing.AttributeValues = incoming.AttributeValues; + + updated++; + } + } + + await _dbContext.SaveChangesAsync(); + + return (created, updated); + } + + public async Task DeleteAsync(List items) + { + var itemsToDelete = _dbContext.InventoryItems.Where(_ => items.Select(_ => _.Id).Contains(_.Id)).ToList(); + + _dbContext.InventoryItems.RemoveRange(itemsToDelete); + + return await _dbContext.SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/WyvernInventory.Infrastructure/Repos/InventoryTypeRepo.cs b/WyvernInventory.Infrastructure/Repos/InventoryTypeRepo.cs new file mode 100644 index 0000000..b649d8c --- /dev/null +++ b/WyvernInventory.Infrastructure/Repos/InventoryTypeRepo.cs @@ -0,0 +1,86 @@ +using Microsoft.EntityFrameworkCore; +using WyvernInventory.Core.Interfaces.Repos; +using WyvernInventory.Core.Models; +using WyvernInventory.Infrastructure.Data; + +namespace WyvernInventory.Infrastructure.Repos; + +public class InventoryTypeRepo(DBContext dbContext) : IDbObjectRepo +{ + private readonly DBContext _dbContext = dbContext; + + public async Task> GetAsync(List? filterList = null) + { + return _dbContext.InventoryTypes + .Include(_ => _.AttributeDefinitions) + .Select(_ => new InventoryTypeDto + { + Id = _.Id, + Name = _.Name, + AttributeDefinitions = _.AttributeDefinitions.Select(x => new InventoryAttributeDefinitionDto + { + Id = x.Id, + Name = x.Name, + DataType = x.DataType + }).ToList() + }) + .ToList(); + } + + public async Task<(int created, int updated)> UpsertAsync(List items) + { + var created = 0; + var updated = 0; + + var itemsToCreate = items + .Where(_ => _.Id is null or 0) + .ToList(); + + var itemsToUpdate = items + .Where(_ => _.Id is not null and not 0) + .ToList(); + + if (itemsToCreate.Count > 0) + { + _dbContext.InventoryTypes.AddRange(itemsToCreate); + created = itemsToCreate.Count; + } + + if (itemsToUpdate.Count > 0) + { + List idList = itemsToUpdate.Select(_ => + { + if (_.Id != null) return (int)_.Id; + throw new NullReferenceException(); + }).ToList(); + + var existingItems = _dbContext.InventoryTypes.ToList() + .Where(_ => idList.Contains((int)_.Id!)) + .ToDictionary(_ => (int)_.Id!); + + foreach (var incoming in itemsToUpdate) + { + if (!existingItems.TryGetValue(incoming.Id!.Value, out var existing)) + continue; + + existing.Name = incoming.Name; + existing.AttributeDefinitions = incoming.AttributeDefinitions; + + updated++; + } + } + + await _dbContext.SaveChangesAsync(); + + return (created, updated); + } + + public async Task DeleteAsync(List items) + { + var itemsToDelete = _dbContext.InventoryTypes.Where(_ => items.Select(_ => _.Id).Contains(_.Id)).ToList(); + + _dbContext.InventoryTypes.RemoveRange(itemsToDelete); + + return await _dbContext.SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/WyvernInventory.Infrastructure/Services/GenericInventoryItemService.cs b/WyvernInventory.Infrastructure/Services/GenericInventoryItemService.cs deleted file mode 100644 index a151f53..0000000 --- a/WyvernInventory.Infrastructure/Services/GenericInventoryItemService.cs +++ /dev/null @@ -1,17 +0,0 @@ -using WyvernInventory.Core.Interfaces.Repos; -using WyvernInventory.Core.Interfaces.Services; -using WyvernInventory.Core.Models; - -namespace WyvernInventory.Infrastructure.Services; - -public class GenericInventoryItemService(IDbObjectRepo dbRepo) - : IDataObjectService -{ - private readonly IDbObjectRepo _dbRepo = dbRepo; - - public async Task> GetAsync(Predicate? filter = null) => await _dbRepo.GetAsync(filter); - - public async Task<(int created, int updated)> UpsertAsync(List items) => await _dbRepo.UpsertAsync(items); - - public async Task DeleteAsync(List items) => await _dbRepo.DeleteAsync(items); -} \ No newline at end of file diff --git a/WyvernInventory.Infrastructure/Services/InventoryItemService.cs b/WyvernInventory.Infrastructure/Services/InventoryItemService.cs new file mode 100644 index 0000000..a34f006 --- /dev/null +++ b/WyvernInventory.Infrastructure/Services/InventoryItemService.cs @@ -0,0 +1,16 @@ +using WyvernInventory.Core.Interfaces.Repos; +using WyvernInventory.Core.Interfaces.Services; +using WyvernInventory.Core.Models; + +namespace WyvernInventory.Infrastructure.Services; + +public class InventoryItemService(IInventoryItemRepo repo) : IInventoryItemService +{ + private IInventoryItemRepo _repo = repo; + + public async Task> GetAsync(List? filter = null) => await _repo.GetAsync(filter); + + public async Task<(int created, int updated)> UpsertAsync(List items) => await _repo.UpsertAsync(items); + + public async Task DeleteAsync(List items) => await _repo.DeleteAsync(items); +} \ No newline at end of file diff --git a/WyvernInventory.Infrastructure/Services/InventoryTypeService.cs b/WyvernInventory.Infrastructure/Services/InventoryTypeService.cs new file mode 100644 index 0000000..96a3d54 --- /dev/null +++ b/WyvernInventory.Infrastructure/Services/InventoryTypeService.cs @@ -0,0 +1,16 @@ +using WyvernInventory.Core.Interfaces.Repos; +using WyvernInventory.Core.Interfaces.Services; +using WyvernInventory.Core.Models; + +namespace WyvernInventory.Infrastructure.Services; + +public class InventoryTypeService(IDbObjectRepo repo) : IDataObjectService +{ + private IDbObjectRepo _repo = repo; + + public async Task> GetAsync(List? filter = null) => await _repo.GetAsync(filter); + + public async Task<(int created, int updated)> UpsertAsync(List items) => await _repo.UpsertAsync(items); + + public async Task DeleteAsync(List items) => await _repo.DeleteAsync(items); +} \ No newline at end of file diff --git a/WyvernInventory.Infrastructure/Utils/DbUtils.cs b/WyvernInventory.Infrastructure/Utils/DbUtils.cs new file mode 100644 index 0000000..14fcbb4 --- /dev/null +++ b/WyvernInventory.Infrastructure/Utils/DbUtils.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.Logging; +using Serilog.Core; +using WyvernInventory.Infrastructure.Data; + +namespace WyvernInventory.Infrastructure.Utils; + +public static class DbUtils +{ + public static bool CanConnectToDatabase(DBContext db, ILogger logger) + { + if (db.Database.CanConnect()) + { + return true; + } + + logger.LogCritical("Unable to connect to DB"); + + Environment.Exit(1); + + return false; + } +} \ No newline at end of file diff --git a/WyvernInventory.Infrastructure/WyvernInventory.Infrastructure.csproj b/WyvernInventory.Infrastructure/WyvernInventory.Infrastructure.csproj index dfc73b9..338b81c 100644 --- a/WyvernInventory.Infrastructure/WyvernInventory.Infrastructure.csproj +++ b/WyvernInventory.Infrastructure/WyvernInventory.Infrastructure.csproj @@ -10,4 +10,12 @@ + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/WyvernInventory.sln b/WyvernInventory.sln index 7095180..88d0762 100644 --- a/WyvernInventory.sln +++ b/WyvernInventory.sln @@ -5,6 +5,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{CE4BF34F-D890-4DCD-BF09-5CAC9C7BD706}" ProjectSection(SolutionItems) = preProject compose.yaml = compose.yaml + .env = .env EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WyvernInventory.Core", "WyvernInventory.Core\WyvernInventory.Core.csproj", "{CE9A8DB9-0746-4653-BBB1-27AC278C79AE}" diff --git a/compose.yaml b/compose.yaml index 0df10ad..0352ae5 100644 --- a/compose.yaml +++ b/compose.yaml @@ -4,4 +4,8 @@ build: context: . dockerfile: WyvernInventory.API/Dockerfile - + ports: + - "5244:8080" + environment: + Database__ConnectionString: "Server=${DB_SERVER};Database=${DB_NAME};User Id=${DB_USER};Password=${DB_PASSWORD};TrustServerCertificate=True;" + TZ: America/Chicago \ No newline at end of file -- 2.52.0