From 4d47f46cdc0e8df9a6057e6c1faf2045e4cc7be1 Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Sun, 23 Mar 2025 13:39:45 -0400 Subject: [PATCH 01/34] initial commit --- .dockerignore | 30 ++++++++++ .../Controllers/WeatherForecastController.cs | 28 ++++++++++ Hippocrates.Journa-WebAPI/Dockerfile | 30 ++++++++++ .../Hippocrates.Journal.WebAPI.csproj | 17 ++++++ .../Hippocrates.Journal.WebAPI.http | 6 ++ Hippocrates.Journa-WebAPI/Program.cs | 24 ++++++++ .../Properties/launchSettings.json | 52 +++++++++++++++++ Hippocrates.Journa-WebAPI/WeatherForecast.cs | 11 ++++ .../appsettings.Development.json | 8 +++ Hippocrates.Journa-WebAPI/appsettings.json | 9 +++ .../BaseEntity.cs | 15 +++++ .../Hippocrates.Journal.DomainEntities.csproj | 10 ++++ .../Intensity.cs | 17 ++++++ .../JournalEntry.cs | 19 +++++++ Hippocrates.Journal-DomainEntities/Symptom.cs | 14 +++++ .../Hippocrates.Journal.CLI.csproj | 11 ++++ Hippocrates.Journal-Service.CLI/Program.cs | 2 + Hippocrates.Journal.sln | 40 +++++++++++++ README.md | 56 ++++++++++++++++++- 19 files changed, 398 insertions(+), 1 deletion(-) create mode 100644 .dockerignore create mode 100644 Hippocrates.Journa-WebAPI/Controllers/WeatherForecastController.cs create mode 100644 Hippocrates.Journa-WebAPI/Dockerfile create mode 100644 Hippocrates.Journa-WebAPI/Hippocrates.Journal.WebAPI.csproj create mode 100644 Hippocrates.Journa-WebAPI/Hippocrates.Journal.WebAPI.http create mode 100644 Hippocrates.Journa-WebAPI/Program.cs create mode 100644 Hippocrates.Journa-WebAPI/Properties/launchSettings.json create mode 100644 Hippocrates.Journa-WebAPI/WeatherForecast.cs create mode 100644 Hippocrates.Journa-WebAPI/appsettings.Development.json create mode 100644 Hippocrates.Journa-WebAPI/appsettings.json create mode 100644 Hippocrates.Journal-DomainEntities/BaseEntity.cs create mode 100644 Hippocrates.Journal-DomainEntities/Hippocrates.Journal.DomainEntities.csproj create mode 100644 Hippocrates.Journal-DomainEntities/Intensity.cs create mode 100644 Hippocrates.Journal-DomainEntities/JournalEntry.cs create mode 100644 Hippocrates.Journal-DomainEntities/Symptom.cs create mode 100644 Hippocrates.Journal-Service.CLI/Hippocrates.Journal.CLI.csproj create mode 100644 Hippocrates.Journal-Service.CLI/Program.cs create mode 100644 Hippocrates.Journal.sln diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..fe1152b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,30 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md +!**/.gitignore +!.git/HEAD +!.git/config +!.git/packed-refs +!.git/refs/heads/** \ No newline at end of file diff --git a/Hippocrates.Journa-WebAPI/Controllers/WeatherForecastController.cs b/Hippocrates.Journa-WebAPI/Controllers/WeatherForecastController.cs new file mode 100644 index 0000000..1395b97 --- /dev/null +++ b/Hippocrates.Journa-WebAPI/Controllers/WeatherForecastController.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Hippocrates.Journal_WebAPI.Controllers { + [ApiController] + [Route("[controller]")] + public class WeatherForecastController : ControllerBase { + private static readonly string[] Summaries = new[] + { + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; + + private readonly ILogger _logger; + + public WeatherForecastController(ILogger logger) { + _logger = logger; + } + + [HttpGet(Name = "GetWeatherForecast")] + public IEnumerable Get() { + 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(); + } + } +} diff --git a/Hippocrates.Journa-WebAPI/Dockerfile b/Hippocrates.Journa-WebAPI/Dockerfile new file mode 100644 index 0000000..eefb74e --- /dev/null +++ b/Hippocrates.Journa-WebAPI/Dockerfile @@ -0,0 +1,30 @@ +# See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. + +# This stage is used when running from VS in fast mode (Default for Debug configuration) +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + + +# This stage is used to build the service project +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["Hippocrates.Journa-WebAPI/Hippocrates.Journa-WebAPI.csproj", "Hippocrates.Journa-WebAPI/"] +RUN dotnet restore "./Hippocrates.Journa-WebAPI/Hippocrates.Journa-WebAPI.csproj" +COPY . . +WORKDIR "/src/Hippocrates.Journa-WebAPI" +RUN dotnet build "./Hippocrates.Journa-WebAPI.csproj" -c $BUILD_CONFIGURATION -o /app/build + +# This stage is used to publish the service project to be copied to the final stage +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./Hippocrates.Journa-WebAPI.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +# This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration) +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Hippocrates.Journa-WebAPI.dll"] \ No newline at end of file diff --git a/Hippocrates.Journa-WebAPI/Hippocrates.Journal.WebAPI.csproj b/Hippocrates.Journa-WebAPI/Hippocrates.Journal.WebAPI.csproj new file mode 100644 index 0000000..8acb6c8 --- /dev/null +++ b/Hippocrates.Journa-WebAPI/Hippocrates.Journal.WebAPI.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + Hippocrates.Journa.WebAPI + 8c63a320-2c3e-49fe-b702-bc5449d6d6fa + Linux + + + + + + + + diff --git a/Hippocrates.Journa-WebAPI/Hippocrates.Journal.WebAPI.http b/Hippocrates.Journa-WebAPI/Hippocrates.Journal.WebAPI.http new file mode 100644 index 0000000..0352af9 --- /dev/null +++ b/Hippocrates.Journa-WebAPI/Hippocrates.Journal.WebAPI.http @@ -0,0 +1,6 @@ +@Hippocrates.Journal.WebAPI_HostAddress = http://localhost:5256 + +GET {{Hippocrates.Journal.WebAPI_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/Hippocrates.Journa-WebAPI/Program.cs b/Hippocrates.Journa-WebAPI/Program.cs new file mode 100644 index 0000000..3229d20 --- /dev/null +++ b/Hippocrates.Journa-WebAPI/Program.cs @@ -0,0 +1,24 @@ +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) { + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/Hippocrates.Journa-WebAPI/Properties/launchSettings.json b/Hippocrates.Journa-WebAPI/Properties/launchSettings.json new file mode 100644 index 0000000..bf7534f --- /dev/null +++ b/Hippocrates.Journa-WebAPI/Properties/launchSettings.json @@ -0,0 +1,52 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5256" + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7083;http://localhost:5256" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Container (Dockerfile)": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", + "environmentVariables": { + "ASPNETCORE_HTTPS_PORTS": "8081", + "ASPNETCORE_HTTP_PORTS": "8080" + }, + "publishAllPorts": true, + "useSSL": true + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:38163", + "sslPort": 44369 + } + } +} \ No newline at end of file diff --git a/Hippocrates.Journa-WebAPI/WeatherForecast.cs b/Hippocrates.Journa-WebAPI/WeatherForecast.cs new file mode 100644 index 0000000..cea0137 --- /dev/null +++ b/Hippocrates.Journa-WebAPI/WeatherForecast.cs @@ -0,0 +1,11 @@ +namespace Hippocrates.Journal_WebAPI { + 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; } + } +} diff --git a/Hippocrates.Journa-WebAPI/appsettings.Development.json b/Hippocrates.Journa-WebAPI/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/Hippocrates.Journa-WebAPI/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Hippocrates.Journa-WebAPI/appsettings.json b/Hippocrates.Journa-WebAPI/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/Hippocrates.Journa-WebAPI/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Hippocrates.Journal-DomainEntities/BaseEntity.cs b/Hippocrates.Journal-DomainEntities/BaseEntity.cs new file mode 100644 index 0000000..ebfc76e --- /dev/null +++ b/Hippocrates.Journal-DomainEntities/BaseEntity.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Hippocrates.Journal_DomainEntities { + public class BaseEntity { + [Required] + public required DateTime CreatedDate { get; set; } + [Required] + public required DateTime UpdatedDate { get; set; } + } +} diff --git a/Hippocrates.Journal-DomainEntities/Hippocrates.Journal.DomainEntities.csproj b/Hippocrates.Journal-DomainEntities/Hippocrates.Journal.DomainEntities.csproj new file mode 100644 index 0000000..6bde7f4 --- /dev/null +++ b/Hippocrates.Journal-DomainEntities/Hippocrates.Journal.DomainEntities.csproj @@ -0,0 +1,10 @@ + + + + net8.0 + Hippocrates.Journal.DomainEntities + enable + enable + + + diff --git a/Hippocrates.Journal-DomainEntities/Intensity.cs b/Hippocrates.Journal-DomainEntities/Intensity.cs new file mode 100644 index 0000000..dbe1de9 --- /dev/null +++ b/Hippocrates.Journal-DomainEntities/Intensity.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; + +namespace Hippocrates.Journal_DomainEntities { + public class Intensity :BaseEntity { + [Key] + public int IntensityId { get; set; } + [Required] + public Guid IntensityGuid { get; set; } + [Required, MaxLength(50)] + public required string Name { get; set; } + [Required] + public required int Level { get; set; } + [MaxLength(500)] + public string? Description { get; set; } + + } +} diff --git a/Hippocrates.Journal-DomainEntities/JournalEntry.cs b/Hippocrates.Journal-DomainEntities/JournalEntry.cs new file mode 100644 index 0000000..c169f17 --- /dev/null +++ b/Hippocrates.Journal-DomainEntities/JournalEntry.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; + +namespace Hippocrates.Journal_DomainEntities { + public class JournalEntry : BaseEntity { + [Key] + public int JournalEntryId { get; set; } + [Required] + public Guid JournalEntryGuid { get; set; } + [Required] + public DateTime Timestamp { get; set; } = DateTime.Now; + [Required] + public virtual required Symptom Symptom { get; set; } + //TODO: add other journal entry types or consider making separate tables + // and just tracking a journal entry type here (food, exercise) + public virtual required Intensity Intensity { get; set; } + [MaxLength(512)] + public string? JournalEntryDescription { get; set; } + } +} diff --git a/Hippocrates.Journal-DomainEntities/Symptom.cs b/Hippocrates.Journal-DomainEntities/Symptom.cs new file mode 100644 index 0000000..48f656a --- /dev/null +++ b/Hippocrates.Journal-DomainEntities/Symptom.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace Hippocrates.Journal_DomainEntities { + public class Symptom : BaseEntity { + [Key] + public int SymptomId { get; set; } + [Required] + public Guid SymptomGuid { get; set; } + [Required] + public required string SymptomName { get; set; } + [MaxLength(100)] + public string? SymptomDescription { get; set; } + } +} diff --git a/Hippocrates.Journal-Service.CLI/Hippocrates.Journal.CLI.csproj b/Hippocrates.Journal-Service.CLI/Hippocrates.Journal.CLI.csproj new file mode 100644 index 0000000..8b4a70f --- /dev/null +++ b/Hippocrates.Journal-Service.CLI/Hippocrates.Journal.CLI.csproj @@ -0,0 +1,11 @@ + + + + Exe + net8.0 + Hippocrates.Journal.CLI + enable + enable + + + diff --git a/Hippocrates.Journal-Service.CLI/Program.cs b/Hippocrates.Journal-Service.CLI/Program.cs new file mode 100644 index 0000000..3751555 --- /dev/null +++ b/Hippocrates.Journal-Service.CLI/Program.cs @@ -0,0 +1,2 @@ +// See https://aka.ms/new-console-template for more information +Console.WriteLine("Hello, World!"); diff --git a/Hippocrates.Journal.sln b/Hippocrates.Journal.sln new file mode 100644 index 0000000..c18d19a --- /dev/null +++ b/Hippocrates.Journal.sln @@ -0,0 +1,40 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.12.35521.163 d17.12 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hippocrates.Journal.CLI", "Hippocrates.Journal-Service.CLI\Hippocrates.Journal.CLI.csproj", "{D0E93737-A883-4020-9C99-E6C1D70D1237}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hippocrates.Journal.WebAPI", "Hippocrates.Journa-WebAPI\Hippocrates.Journal.WebAPI.csproj", "{D1AF623E-A97D-4CD7-81CA-868F4D6EEEF8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hippocrates.Journal.DomainEntities", "Hippocrates.Journal-DomainEntities\Hippocrates.Journal.DomainEntities.csproj", "{2F884BE1-F88D-45F7-A3E0-480FCA94541A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{994FEE9A-A85B-4AEC-894B-CD7FEDF3FC09}" + ProjectSection(SolutionItems) = preProject + LICENSE = LICENSE + README.md = README.md + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D0E93737-A883-4020-9C99-E6C1D70D1237}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D0E93737-A883-4020-9C99-E6C1D70D1237}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D0E93737-A883-4020-9C99-E6C1D70D1237}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D0E93737-A883-4020-9C99-E6C1D70D1237}.Release|Any CPU.Build.0 = Release|Any CPU + {D1AF623E-A97D-4CD7-81CA-868F4D6EEEF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1AF623E-A97D-4CD7-81CA-868F4D6EEEF8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1AF623E-A97D-4CD7-81CA-868F4D6EEEF8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1AF623E-A97D-4CD7-81CA-868F4D6EEEF8}.Release|Any CPU.Build.0 = Release|Any CPU + {2F884BE1-F88D-45F7-A3E0-480FCA94541A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F884BE1-F88D-45F7-A3E0-480FCA94541A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F884BE1-F88D-45F7-A3E0-480FCA94541A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F884BE1-F88D-45F7-A3E0-480FCA94541A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md index cfceac2..6104d0b 100644 --- a/README.md +++ b/README.md @@ -1 +1,55 @@ -# HippocratesJournal \ No newline at end of file +# Hippocrates Journal +Collect basic timestamped symptom data with the intent that the data can be analyzed/mined to see longer term patterns. + +To start, this will focus on symptoms and intensity and intentionally be a single user app. Longer term, the idea is to be able to couple this data with other similar journal data (such as food and medicine tracking). With this in mind the journal entries will be fairly generic in nature. +Maybe rather than tracking symptoms, food, exercise separately, we track EVENTS +with user defined event types such as +- Bathroom visit with sub events such as + - blood in stool + - stool consistency + - back pain prior to event +- Exercise with sub events like + - ?? +drawback here is abstraction complexity and different events have different needs +(symptoms for some events, duration and other meta data for exercises, both have intensity but the intensities have different meanings) + + +OR we track each event type in a separate table with separate journal tables? +- could use inheritance and have a base journal entry class or interface +- drawback is requires new tables for every new event type + +OR + +maybe both? have events and some events have related symptoms + +---------- + + +## Features + + +Possible features include: +User definable symptom lists +User definable symptom intensity descriptions (tied to numeric values for graphing/data presentation purposes) + +#### Pain level could be something like + +- None (0) +- Slight to mild Discomfort (1) +- I need an OTC painkiller (2) +- I'm going to limit my activity (3) +- I can't function (4) +- Take me to the ER (5) +#### Bleeding could be +- None (0) +- Trace (on TP only) (1) +- Minor (drops visible in water) (2) +- Major (water mostly red) (3) +- Intense (water completly red) (4) + +journal entries should also have user defined tags +not sure how to implement tags jsut yet, maybe a tag entity and table +and a tags list table (with an id) and have journal entry track taglist id + + + From 810e40e61044714d3c6f22720224328ab30ed087 Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Sun, 30 Mar 2025 15:37:11 -0400 Subject: [PATCH 02/34] Code initial entities, set project structure --- .../BaseEntity.cs | 15 ++++-------- Hippocrates.Journal-DomainEntities/Event.cs | 10 ++++++++ .../EventSymptom.cs | 17 +++++++++++++ .../EventType.cs | 24 +++++++++++++++++++ .../Intensity.cs | 23 +++++++++--------- .../JournalEntry.cs | 19 --------------- Hippocrates.Journal-DomainEntities/Symptom.cs | 14 ++++------- .../Controllers/WeatherForecastController.cs | 2 +- .../Dockerfile | 0 .../Hippocrates.Journal.WebAPI.csproj | 0 .../Hippocrates.Journal.WebAPI.http | 0 .../Program.cs | 0 .../Properties/launchSettings.json | 0 .../WeatherForecast.cs | 2 +- .../appsettings.Development.json | 0 .../appsettings.json | 0 README.md | 19 --------------- 17 files changed, 73 insertions(+), 72 deletions(-) create mode 100644 Hippocrates.Journal-DomainEntities/Event.cs create mode 100644 Hippocrates.Journal-DomainEntities/EventSymptom.cs create mode 100644 Hippocrates.Journal-DomainEntities/EventType.cs delete mode 100644 Hippocrates.Journal-DomainEntities/JournalEntry.cs rename {Hippocrates.Journa-WebAPI => Hippocrates.Journal-WebAPI}/Controllers/WeatherForecastController.cs (95%) rename {Hippocrates.Journa-WebAPI => Hippocrates.Journal-WebAPI}/Dockerfile (100%) rename {Hippocrates.Journa-WebAPI => Hippocrates.Journal-WebAPI}/Hippocrates.Journal.WebAPI.csproj (100%) rename {Hippocrates.Journa-WebAPI => Hippocrates.Journal-WebAPI}/Hippocrates.Journal.WebAPI.http (100%) rename {Hippocrates.Journa-WebAPI => Hippocrates.Journal-WebAPI}/Program.cs (100%) rename {Hippocrates.Journa-WebAPI => Hippocrates.Journal-WebAPI}/Properties/launchSettings.json (100%) rename {Hippocrates.Journa-WebAPI => Hippocrates.Journal-WebAPI}/WeatherForecast.cs (86%) rename {Hippocrates.Journa-WebAPI => Hippocrates.Journal-WebAPI}/appsettings.Development.json (100%) rename {Hippocrates.Journa-WebAPI => Hippocrates.Journal-WebAPI}/appsettings.json (100%) diff --git a/Hippocrates.Journal-DomainEntities/BaseEntity.cs b/Hippocrates.Journal-DomainEntities/BaseEntity.cs index ebfc76e..93f322c 100644 --- a/Hippocrates.Journal-DomainEntities/BaseEntity.cs +++ b/Hippocrates.Journal-DomainEntities/BaseEntity.cs @@ -1,15 +1,8 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.ComponentModel.DataAnnotations; -namespace Hippocrates.Journal_DomainEntities { +namespace Hippocrates.Journal.DomainEntities { public class BaseEntity { - [Required] - public required DateTime CreatedDate { get; set; } - [Required] - public required DateTime UpdatedDate { get; set; } + [Required] public DateTime CreatedDate { get; set; } = DateTime.Now; + [Required] public DateTime UpdatedDate { get; set; } = DateTime.MinValue; } } diff --git a/Hippocrates.Journal-DomainEntities/Event.cs b/Hippocrates.Journal-DomainEntities/Event.cs new file mode 100644 index 0000000..b89692c --- /dev/null +++ b/Hippocrates.Journal-DomainEntities/Event.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace Hippocrates.Journal.DomainEntities { + public class Event : BaseEntity { + [Key] public int EventId { get; set; } + [Required] public Guid EventGuid { get; set; } + [Required] public DateTime Timestamp { get; set; } = DateTime.Now; + [MaxLength(500)] public string? Description { get; set; } + } +} diff --git a/Hippocrates.Journal-DomainEntities/EventSymptom.cs b/Hippocrates.Journal-DomainEntities/EventSymptom.cs new file mode 100644 index 0000000..4b0ddc2 --- /dev/null +++ b/Hippocrates.Journal-DomainEntities/EventSymptom.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Hippocrates.Journal.DomainEntities { + public class EventSymptom : BaseEntity{ + [Key] public int EventSymptomId { get; set; } + [Required] public Guid EventSymptomGuid { get; set; } + [Required] public virtual required Event Event { get; set; } + [Required] public virtual required Symptom Symptom { get; set; } + [Required] public virtual required Intensity Intensity { get; set; } + [MaxLength(512)] public string? Notes { get; set; } + } +} diff --git a/Hippocrates.Journal-DomainEntities/EventType.cs b/Hippocrates.Journal-DomainEntities/EventType.cs new file mode 100644 index 0000000..9967d28 --- /dev/null +++ b/Hippocrates.Journal-DomainEntities/EventType.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Hippocrates.Journal.DomainEntities { + public class EventType : BaseEntity { + [Key] public int EventTypeId { get; set; } + [Required] public Guid EventTypeGuid { get; set; } + [Required] public required string Name { get; set; } + [MaxLength(500)] public string? Description { get; set; } + + public static IEnumerable DefaultEventTypes() { + return [ + new EventType { EventTypeGuid = Guid.NewGuid(), Name = "Random Event", Description = "Use for tracking random things like onset of pain, headache, or whatever that isn't directly associated with a specific even type."}, + new EventType { EventTypeGuid = Guid.NewGuid(), Name = "Exercise" }, + new EventType { EventTypeGuid = Guid.NewGuid(), Name = "Bathroom visit" }, + new EventType { EventTypeGuid = Guid.NewGuid(), Name = "Meal" }, + ]; + } + } +} diff --git a/Hippocrates.Journal-DomainEntities/Intensity.cs b/Hippocrates.Journal-DomainEntities/Intensity.cs index dbe1de9..c739296 100644 --- a/Hippocrates.Journal-DomainEntities/Intensity.cs +++ b/Hippocrates.Journal-DomainEntities/Intensity.cs @@ -1,17 +1,16 @@ using System.ComponentModel.DataAnnotations; -namespace Hippocrates.Journal_DomainEntities { +namespace Hippocrates.Journal.DomainEntities { public class Intensity :BaseEntity { - [Key] - public int IntensityId { get; set; } - [Required] - public Guid IntensityGuid { get; set; } - [Required, MaxLength(50)] - public required string Name { get; set; } - [Required] - public required int Level { get; set; } - [MaxLength(500)] - public string? Description { get; set; } - + [Key] public int IntensityId { get; set; } + [Required] public Guid IntensityGuid { get; set; } + [Required, MaxLength(50)] public required string Name { get; set; } + [Required] public required int Level { get; set; } + [MaxLength(500)] public string? Description { get; set; } + [Required] public required SortType SortType { get; set; } + } + //TODO: move this somewhere more sensible + public enum SortType { + Custom, Ascending, Descending } } diff --git a/Hippocrates.Journal-DomainEntities/JournalEntry.cs b/Hippocrates.Journal-DomainEntities/JournalEntry.cs deleted file mode 100644 index c169f17..0000000 --- a/Hippocrates.Journal-DomainEntities/JournalEntry.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Hippocrates.Journal_DomainEntities { - public class JournalEntry : BaseEntity { - [Key] - public int JournalEntryId { get; set; } - [Required] - public Guid JournalEntryGuid { get; set; } - [Required] - public DateTime Timestamp { get; set; } = DateTime.Now; - [Required] - public virtual required Symptom Symptom { get; set; } - //TODO: add other journal entry types or consider making separate tables - // and just tracking a journal entry type here (food, exercise) - public virtual required Intensity Intensity { get; set; } - [MaxLength(512)] - public string? JournalEntryDescription { get; set; } - } -} diff --git a/Hippocrates.Journal-DomainEntities/Symptom.cs b/Hippocrates.Journal-DomainEntities/Symptom.cs index 48f656a..4661eab 100644 --- a/Hippocrates.Journal-DomainEntities/Symptom.cs +++ b/Hippocrates.Journal-DomainEntities/Symptom.cs @@ -1,14 +1,10 @@ using System.ComponentModel.DataAnnotations; -namespace Hippocrates.Journal_DomainEntities { +namespace Hippocrates.Journal.DomainEntities { public class Symptom : BaseEntity { - [Key] - public int SymptomId { get; set; } - [Required] - public Guid SymptomGuid { get; set; } - [Required] - public required string SymptomName { get; set; } - [MaxLength(100)] - public string? SymptomDescription { get; set; } + [Key] public int SymptomId { get; set; } + [Required] public Guid SymptomGuid { get; set; } + [Required] public required string SymptomName { get; set; } + [MaxLength(500)] public string? SymptomDescription { get; set; } } } diff --git a/Hippocrates.Journa-WebAPI/Controllers/WeatherForecastController.cs b/Hippocrates.Journal-WebAPI/Controllers/WeatherForecastController.cs similarity index 95% rename from Hippocrates.Journa-WebAPI/Controllers/WeatherForecastController.cs rename to Hippocrates.Journal-WebAPI/Controllers/WeatherForecastController.cs index 1395b97..aa8f286 100644 --- a/Hippocrates.Journa-WebAPI/Controllers/WeatherForecastController.cs +++ b/Hippocrates.Journal-WebAPI/Controllers/WeatherForecastController.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Mvc; -namespace Hippocrates.Journal_WebAPI.Controllers { +namespace Hippocrates.Journal.WebAPI.Controllers { [ApiController] [Route("[controller]")] public class WeatherForecastController : ControllerBase { diff --git a/Hippocrates.Journa-WebAPI/Dockerfile b/Hippocrates.Journal-WebAPI/Dockerfile similarity index 100% rename from Hippocrates.Journa-WebAPI/Dockerfile rename to Hippocrates.Journal-WebAPI/Dockerfile diff --git a/Hippocrates.Journa-WebAPI/Hippocrates.Journal.WebAPI.csproj b/Hippocrates.Journal-WebAPI/Hippocrates.Journal.WebAPI.csproj similarity index 100% rename from Hippocrates.Journa-WebAPI/Hippocrates.Journal.WebAPI.csproj rename to Hippocrates.Journal-WebAPI/Hippocrates.Journal.WebAPI.csproj diff --git a/Hippocrates.Journa-WebAPI/Hippocrates.Journal.WebAPI.http b/Hippocrates.Journal-WebAPI/Hippocrates.Journal.WebAPI.http similarity index 100% rename from Hippocrates.Journa-WebAPI/Hippocrates.Journal.WebAPI.http rename to Hippocrates.Journal-WebAPI/Hippocrates.Journal.WebAPI.http diff --git a/Hippocrates.Journa-WebAPI/Program.cs b/Hippocrates.Journal-WebAPI/Program.cs similarity index 100% rename from Hippocrates.Journa-WebAPI/Program.cs rename to Hippocrates.Journal-WebAPI/Program.cs diff --git a/Hippocrates.Journa-WebAPI/Properties/launchSettings.json b/Hippocrates.Journal-WebAPI/Properties/launchSettings.json similarity index 100% rename from Hippocrates.Journa-WebAPI/Properties/launchSettings.json rename to Hippocrates.Journal-WebAPI/Properties/launchSettings.json diff --git a/Hippocrates.Journa-WebAPI/WeatherForecast.cs b/Hippocrates.Journal-WebAPI/WeatherForecast.cs similarity index 86% rename from Hippocrates.Journa-WebAPI/WeatherForecast.cs rename to Hippocrates.Journal-WebAPI/WeatherForecast.cs index cea0137..2bd2c71 100644 --- a/Hippocrates.Journa-WebAPI/WeatherForecast.cs +++ b/Hippocrates.Journal-WebAPI/WeatherForecast.cs @@ -1,4 +1,4 @@ -namespace Hippocrates.Journal_WebAPI { +namespace Hippocrates.Journal.WebAPI { public class WeatherForecast { public DateOnly Date { get; set; } diff --git a/Hippocrates.Journa-WebAPI/appsettings.Development.json b/Hippocrates.Journal-WebAPI/appsettings.Development.json similarity index 100% rename from Hippocrates.Journa-WebAPI/appsettings.Development.json rename to Hippocrates.Journal-WebAPI/appsettings.Development.json diff --git a/Hippocrates.Journa-WebAPI/appsettings.json b/Hippocrates.Journal-WebAPI/appsettings.json similarity index 100% rename from Hippocrates.Journa-WebAPI/appsettings.json rename to Hippocrates.Journal-WebAPI/appsettings.json diff --git a/README.md b/README.md index 6104d0b..05fa650 100644 --- a/README.md +++ b/README.md @@ -4,25 +4,6 @@ Collect basic timestamped symptom data with the intent that the data can be anal To start, this will focus on symptoms and intensity and intentionally be a single user app. Longer term, the idea is to be able to couple this data with other similar journal data (such as food and medicine tracking). With this in mind the journal entries will be fairly generic in nature. Maybe rather than tracking symptoms, food, exercise separately, we track EVENTS with user defined event types such as -- Bathroom visit with sub events such as - - blood in stool - - stool consistency - - back pain prior to event -- Exercise with sub events like - - ?? -drawback here is abstraction complexity and different events have different needs -(symptoms for some events, duration and other meta data for exercises, both have intensity but the intensities have different meanings) - - -OR we track each event type in a separate table with separate journal tables? -- could use inheritance and have a base journal entry class or interface -- drawback is requires new tables for every new event type - -OR - -maybe both? have events and some events have related symptoms - ----------- ## Features From db8235055307f1f0dc1ad6a975d4d24a62587415 Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Sun, 30 Mar 2025 15:53:13 -0400 Subject: [PATCH 03/34] more intial setup --- Hippocrates.Journal-DomainEntities/EventSymptom.cs | 7 +------ Hippocrates.Journal-DomainEntities/EventType.cs | 8 ++------ Hippocrates.Journal.sln | 14 +++++++------- 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/Hippocrates.Journal-DomainEntities/EventSymptom.cs b/Hippocrates.Journal-DomainEntities/EventSymptom.cs index 4b0ddc2..05922bc 100644 --- a/Hippocrates.Journal-DomainEntities/EventSymptom.cs +++ b/Hippocrates.Journal-DomainEntities/EventSymptom.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.ComponentModel.DataAnnotations; namespace Hippocrates.Journal.DomainEntities { public class EventSymptom : BaseEntity{ diff --git a/Hippocrates.Journal-DomainEntities/EventType.cs b/Hippocrates.Journal-DomainEntities/EventType.cs index 9967d28..7fae7f6 100644 --- a/Hippocrates.Journal-DomainEntities/EventType.cs +++ b/Hippocrates.Journal-DomainEntities/EventType.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.ComponentModel.DataAnnotations; namespace Hippocrates.Journal.DomainEntities { public class EventType : BaseEntity { @@ -12,6 +7,7 @@ public class EventType : BaseEntity { [Required] public required string Name { get; set; } [MaxLength(500)] public string? Description { get; set; } + //TODO: this code really belongs in a service or maybe repo public static IEnumerable DefaultEventTypes() { return [ new EventType { EventTypeGuid = Guid.NewGuid(), Name = "Random Event", Description = "Use for tracking random things like onset of pain, headache, or whatever that isn't directly associated with a specific even type."}, diff --git a/Hippocrates.Journal.sln b/Hippocrates.Journal.sln index c18d19a..aa9c601 100644 --- a/Hippocrates.Journal.sln +++ b/Hippocrates.Journal.sln @@ -1,12 +1,10 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.12.35521.163 d17.12 +VisualStudioVersion = 17.12.35521.163 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hippocrates.Journal.CLI", "Hippocrates.Journal-Service.CLI\Hippocrates.Journal.CLI.csproj", "{D0E93737-A883-4020-9C99-E6C1D70D1237}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hippocrates.Journal.WebAPI", "Hippocrates.Journa-WebAPI\Hippocrates.Journal.WebAPI.csproj", "{D1AF623E-A97D-4CD7-81CA-868F4D6EEEF8}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hippocrates.Journal.DomainEntities", "Hippocrates.Journal-DomainEntities\Hippocrates.Journal.DomainEntities.csproj", "{2F884BE1-F88D-45F7-A3E0-480FCA94541A}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{994FEE9A-A85B-4AEC-894B-CD7FEDF3FC09}" @@ -15,6 +13,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hippocrates.Journal.WebAPI", "Hippocrates.Journal-WebAPI\Hippocrates.Journal.WebAPI.csproj", "{AFC2B8DD-6CDE-450F-9181-93B2B67983D7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -25,14 +25,14 @@ Global {D0E93737-A883-4020-9C99-E6C1D70D1237}.Debug|Any CPU.Build.0 = Debug|Any CPU {D0E93737-A883-4020-9C99-E6C1D70D1237}.Release|Any CPU.ActiveCfg = Release|Any CPU {D0E93737-A883-4020-9C99-E6C1D70D1237}.Release|Any CPU.Build.0 = Release|Any CPU - {D1AF623E-A97D-4CD7-81CA-868F4D6EEEF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D1AF623E-A97D-4CD7-81CA-868F4D6EEEF8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D1AF623E-A97D-4CD7-81CA-868F4D6EEEF8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D1AF623E-A97D-4CD7-81CA-868F4D6EEEF8}.Release|Any CPU.Build.0 = Release|Any CPU {2F884BE1-F88D-45F7-A3E0-480FCA94541A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2F884BE1-F88D-45F7-A3E0-480FCA94541A}.Debug|Any CPU.Build.0 = Debug|Any CPU {2F884BE1-F88D-45F7-A3E0-480FCA94541A}.Release|Any CPU.ActiveCfg = Release|Any CPU {2F884BE1-F88D-45F7-A3E0-480FCA94541A}.Release|Any CPU.Build.0 = Release|Any CPU + {AFC2B8DD-6CDE-450F-9181-93B2B67983D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AFC2B8DD-6CDE-450F-9181-93B2B67983D7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AFC2B8DD-6CDE-450F-9181-93B2B67983D7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AFC2B8DD-6CDE-450F-9181-93B2B67983D7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 61822fd0d2f885fd235170666e293651df1255eb Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Sun, 6 Apr 2025 14:13:21 -0400 Subject: [PATCH 04/34] add ef core - still needs pieces --- .../Intensity.cs | 2 +- .../Hippocrates.Journal.WebAPI.csproj | 4 ++-- Hippocrates.Journal.Data/DatabaseContext.cs | 22 +++++++++++++++++++ Hippocrates.Journal.Data/EventRepository.cs | 5 +++++ .../Hippocrates.Journal.Data.csproj | 17 ++++++++++++++ Hippocrates.Journal.Data/SymptomRepository.cs | 10 +++++++++ Hippocrates.Journal.sln | 6 +++++ 7 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 Hippocrates.Journal.Data/DatabaseContext.cs create mode 100644 Hippocrates.Journal.Data/EventRepository.cs create mode 100644 Hippocrates.Journal.Data/Hippocrates.Journal.Data.csproj create mode 100644 Hippocrates.Journal.Data/SymptomRepository.cs diff --git a/Hippocrates.Journal-DomainEntities/Intensity.cs b/Hippocrates.Journal-DomainEntities/Intensity.cs index c739296..943ef71 100644 --- a/Hippocrates.Journal-DomainEntities/Intensity.cs +++ b/Hippocrates.Journal-DomainEntities/Intensity.cs @@ -7,7 +7,7 @@ public class Intensity :BaseEntity { [Required, MaxLength(50)] public required string Name { get; set; } [Required] public required int Level { get; set; } [MaxLength(500)] public string? Description { get; set; } - [Required] public required SortType SortType { get; set; } + [Required] public required SortType DefaultSortType { get; set; } } //TODO: move this somewhere more sensible public enum SortType { diff --git a/Hippocrates.Journal-WebAPI/Hippocrates.Journal.WebAPI.csproj b/Hippocrates.Journal-WebAPI/Hippocrates.Journal.WebAPI.csproj index 8acb6c8..444bf18 100644 --- a/Hippocrates.Journal-WebAPI/Hippocrates.Journal.WebAPI.csproj +++ b/Hippocrates.Journal-WebAPI/Hippocrates.Journal.WebAPI.csproj @@ -10,8 +10,8 @@ - - + + diff --git a/Hippocrates.Journal.Data/DatabaseContext.cs b/Hippocrates.Journal.Data/DatabaseContext.cs new file mode 100644 index 0000000..b29d488 --- /dev/null +++ b/Hippocrates.Journal.Data/DatabaseContext.cs @@ -0,0 +1,22 @@ +using Hippocrates.Journal.DomainEntities; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Hippocrates.Journal.Data { + + public class DatabaseContext { + public DbSet Symptoms { get; set; } + public DbSet Events { get; set; } + public DatabaseContext() { + // needed by ef clit tools + } + + public DatabaseContext() { + + } + } +} diff --git a/Hippocrates.Journal.Data/EventRepository.cs b/Hippocrates.Journal.Data/EventRepository.cs new file mode 100644 index 0000000..e1987ff --- /dev/null +++ b/Hippocrates.Journal.Data/EventRepository.cs @@ -0,0 +1,5 @@ +namespace Hippocrates.Journal.Data { + public class EventRepository { + + } +} diff --git a/Hippocrates.Journal.Data/Hippocrates.Journal.Data.csproj b/Hippocrates.Journal.Data/Hippocrates.Journal.Data.csproj new file mode 100644 index 0000000..01ef1e8 --- /dev/null +++ b/Hippocrates.Journal.Data/Hippocrates.Journal.Data.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/Hippocrates.Journal.Data/SymptomRepository.cs b/Hippocrates.Journal.Data/SymptomRepository.cs new file mode 100644 index 0000000..6ca7844 --- /dev/null +++ b/Hippocrates.Journal.Data/SymptomRepository.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Hippocrates.Journal.Data { + internal class SymptomRepository { + } +} diff --git a/Hippocrates.Journal.sln b/Hippocrates.Journal.sln index aa9c601..55cc7e7 100644 --- a/Hippocrates.Journal.sln +++ b/Hippocrates.Journal.sln @@ -15,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hippocrates.Journal.WebAPI", "Hippocrates.Journal-WebAPI\Hippocrates.Journal.WebAPI.csproj", "{AFC2B8DD-6CDE-450F-9181-93B2B67983D7}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hippocrates.Journal.Data", "Hippocrates.Journal.Data\Hippocrates.Journal.Data.csproj", "{CD1F9E2E-4F39-4E6C-B018-B3BF858D57A5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -33,6 +35,10 @@ Global {AFC2B8DD-6CDE-450F-9181-93B2B67983D7}.Debug|Any CPU.Build.0 = Debug|Any CPU {AFC2B8DD-6CDE-450F-9181-93B2B67983D7}.Release|Any CPU.ActiveCfg = Release|Any CPU {AFC2B8DD-6CDE-450F-9181-93B2B67983D7}.Release|Any CPU.Build.0 = Release|Any CPU + {CD1F9E2E-4F39-4E6C-B018-B3BF858D57A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CD1F9E2E-4F39-4E6C-B018-B3BF858D57A5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CD1F9E2E-4F39-4E6C-B018-B3BF858D57A5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CD1F9E2E-4F39-4E6C-B018-B3BF858D57A5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 8fba5462236739b8a05dd739721dc8a6ca4176c0 Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Sun, 6 Apr 2025 22:21:11 -0400 Subject: [PATCH 05/34] add stubbed repos and base repository --- .../BaseEntity.cs | 7 ++- .../Enums/SortType.cs | 5 +++ Hippocrates.Journal-DomainEntities/Event.cs | 4 +- .../EventSymptom.cs | 4 +- .../EventType.cs | 12 +++--- .../Hippocrates.Journal.DomainEntities.csproj | 4 ++ .../Intensity.cs | 12 +++--- Hippocrates.Journal-DomainEntities/Symptom.cs | 4 +- .../Hippocrates.Journal.CLI.csproj | 13 ++++++ Hippocrates.Journal-Service.CLI/Program.cs | 37 +++++++++++++++- .../Hippocrates.Journal.WebAPI.csproj | 4 ++ Hippocrates.Journal-WebAPI/Program.cs | 36 +++++++++------- Hippocrates.Journal.Data/BaseRepository.cs | 43 +++++++++++++++++++ Hippocrates.Journal.Data/DatabaseContext.cs | 40 ++++++++++++----- Hippocrates.Journal.Data/EventRepository.cs | 18 +++++++- .../EventSymptomRepository.cs | 6 +++ .../EventTypeRepository.cs | 6 +++ .../Hippocrates.Journal.Data.csproj | 4 ++ Hippocrates.Journal.Data/IDatabaseContext.cs | 14 ++++++ Hippocrates.Journal.Data/IntensityReposity.cs | 6 +++ Hippocrates.Journal.Data/SymptomRepository.cs | 9 ++-- 21 files changed, 232 insertions(+), 56 deletions(-) create mode 100644 Hippocrates.Journal-DomainEntities/Enums/SortType.cs create mode 100644 Hippocrates.Journal.Data/BaseRepository.cs create mode 100644 Hippocrates.Journal.Data/EventSymptomRepository.cs create mode 100644 Hippocrates.Journal.Data/EventTypeRepository.cs create mode 100644 Hippocrates.Journal.Data/IDatabaseContext.cs create mode 100644 Hippocrates.Journal.Data/IntensityReposity.cs diff --git a/Hippocrates.Journal-DomainEntities/BaseEntity.cs b/Hippocrates.Journal-DomainEntities/BaseEntity.cs index 93f322c..8847196 100644 --- a/Hippocrates.Journal-DomainEntities/BaseEntity.cs +++ b/Hippocrates.Journal-DomainEntities/BaseEntity.cs @@ -1,8 +1,13 @@ using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; namespace Hippocrates.Journal.DomainEntities { - public class BaseEntity { + public abstract class BaseEntity { [Required] public DateTime CreatedDate { get; set; } = DateTime.Now; [Required] public DateTime UpdatedDate { get; set; } = DateTime.MinValue; + [NotMapped] + [Required] public int Id { get; set; } + [NotMapped] + [Required] public Guid ResourceId { get; set; } } } diff --git a/Hippocrates.Journal-DomainEntities/Enums/SortType.cs b/Hippocrates.Journal-DomainEntities/Enums/SortType.cs new file mode 100644 index 0000000..86e1b38 --- /dev/null +++ b/Hippocrates.Journal-DomainEntities/Enums/SortType.cs @@ -0,0 +1,5 @@ +namespace Hippocrates.Journal.DomainEntities.Enums { + public enum SortType { + Custom, Ascending, Descending + } +} diff --git a/Hippocrates.Journal-DomainEntities/Event.cs b/Hippocrates.Journal-DomainEntities/Event.cs index b89692c..f2138ab 100644 --- a/Hippocrates.Journal-DomainEntities/Event.cs +++ b/Hippocrates.Journal-DomainEntities/Event.cs @@ -2,8 +2,8 @@ namespace Hippocrates.Journal.DomainEntities { public class Event : BaseEntity { - [Key] public int EventId { get; set; } - [Required] public Guid EventGuid { get; set; } + [Key] public int EventId { get { return Id; } set { Id = value; } } + [Required] public Guid EventResourceId { get { return ResourceId; } set { ResourceId = value; } } [Required] public DateTime Timestamp { get; set; } = DateTime.Now; [MaxLength(500)] public string? Description { get; set; } } diff --git a/Hippocrates.Journal-DomainEntities/EventSymptom.cs b/Hippocrates.Journal-DomainEntities/EventSymptom.cs index 05922bc..b4a030d 100644 --- a/Hippocrates.Journal-DomainEntities/EventSymptom.cs +++ b/Hippocrates.Journal-DomainEntities/EventSymptom.cs @@ -2,8 +2,8 @@ namespace Hippocrates.Journal.DomainEntities { public class EventSymptom : BaseEntity{ - [Key] public int EventSymptomId { get; set; } - [Required] public Guid EventSymptomGuid { get; set; } + [Key] public int EventSymptomId { get { return Id; } set { Id = value; } } + [Required] public Guid EventSymptomResourceId { get { return ResourceId; } set { ResourceId = value; } } [Required] public virtual required Event Event { get; set; } [Required] public virtual required Symptom Symptom { get; set; } [Required] public virtual required Intensity Intensity { get; set; } diff --git a/Hippocrates.Journal-DomainEntities/EventType.cs b/Hippocrates.Journal-DomainEntities/EventType.cs index 7fae7f6..7dbb565 100644 --- a/Hippocrates.Journal-DomainEntities/EventType.cs +++ b/Hippocrates.Journal-DomainEntities/EventType.cs @@ -2,18 +2,18 @@ namespace Hippocrates.Journal.DomainEntities { public class EventType : BaseEntity { - [Key] public int EventTypeId { get; set; } - [Required] public Guid EventTypeGuid { get; set; } + [Key] public int EventTypeId { get { return Id; } set { Id = value; } } + [Required] public Guid EventTypeResourceId { get { return ResourceId; } set { ResourceId = value; } } [Required] public required string Name { get; set; } [MaxLength(500)] public string? Description { get; set; } //TODO: this code really belongs in a service or maybe repo public static IEnumerable DefaultEventTypes() { return [ - new EventType { EventTypeGuid = Guid.NewGuid(), Name = "Random Event", Description = "Use for tracking random things like onset of pain, headache, or whatever that isn't directly associated with a specific even type."}, - new EventType { EventTypeGuid = Guid.NewGuid(), Name = "Exercise" }, - new EventType { EventTypeGuid = Guid.NewGuid(), Name = "Bathroom visit" }, - new EventType { EventTypeGuid = Guid.NewGuid(), Name = "Meal" }, + new EventType { EventTypeResourceId = Guid.NewGuid(), Name = "Random Event", Description = "Use for tracking random things like onset of pain, headache, or whatever that isn't directly associated with a specific even type."}, + new EventType { EventTypeResourceId = Guid.NewGuid(), Name = "Exercise" }, + new EventType { EventTypeResourceId = Guid.NewGuid(), Name = "Bathroom visit" }, + new EventType { EventTypeResourceId = Guid.NewGuid(), Name = "Meal" }, ]; } } diff --git a/Hippocrates.Journal-DomainEntities/Hippocrates.Journal.DomainEntities.csproj b/Hippocrates.Journal-DomainEntities/Hippocrates.Journal.DomainEntities.csproj index 6bde7f4..623aa14 100644 --- a/Hippocrates.Journal-DomainEntities/Hippocrates.Journal.DomainEntities.csproj +++ b/Hippocrates.Journal-DomainEntities/Hippocrates.Journal.DomainEntities.csproj @@ -7,4 +7,8 @@ enable + + + + diff --git a/Hippocrates.Journal-DomainEntities/Intensity.cs b/Hippocrates.Journal-DomainEntities/Intensity.cs index 943ef71..f205241 100644 --- a/Hippocrates.Journal-DomainEntities/Intensity.cs +++ b/Hippocrates.Journal-DomainEntities/Intensity.cs @@ -1,16 +1,14 @@ -using System.ComponentModel.DataAnnotations; +using Hippocrates.Journal.DomainEntities.Enums; +using System.ComponentModel.DataAnnotations; namespace Hippocrates.Journal.DomainEntities { public class Intensity :BaseEntity { - [Key] public int IntensityId { get; set; } - [Required] public Guid IntensityGuid { get; set; } + [Key] public int IntensityId { get { return Id; } set { Id = value; } } + [Required] public Guid IntensityResourceId { get { return ResourceId; } set { ResourceId = value; } } [Required, MaxLength(50)] public required string Name { get; set; } [Required] public required int Level { get; set; } [MaxLength(500)] public string? Description { get; set; } [Required] public required SortType DefaultSortType { get; set; } } - //TODO: move this somewhere more sensible - public enum SortType { - Custom, Ascending, Descending - } + } diff --git a/Hippocrates.Journal-DomainEntities/Symptom.cs b/Hippocrates.Journal-DomainEntities/Symptom.cs index 4661eab..adadbf0 100644 --- a/Hippocrates.Journal-DomainEntities/Symptom.cs +++ b/Hippocrates.Journal-DomainEntities/Symptom.cs @@ -2,8 +2,8 @@ namespace Hippocrates.Journal.DomainEntities { public class Symptom : BaseEntity { - [Key] public int SymptomId { get; set; } - [Required] public Guid SymptomGuid { get; set; } + [Key] public int SymptomId { get { return Id; } set { Id = value; } } + [Required] public Guid SymptomResourceId { get { return ResourceId; } set { ResourceId = value; } } [Required] public required string SymptomName { get; set; } [MaxLength(500)] public string? SymptomDescription { get; set; } } diff --git a/Hippocrates.Journal-Service.CLI/Hippocrates.Journal.CLI.csproj b/Hippocrates.Journal-Service.CLI/Hippocrates.Journal.CLI.csproj index 8b4a70f..1d931e4 100644 --- a/Hippocrates.Journal-Service.CLI/Hippocrates.Journal.CLI.csproj +++ b/Hippocrates.Journal-Service.CLI/Hippocrates.Journal.CLI.csproj @@ -8,4 +8,17 @@ enable + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/Hippocrates.Journal-Service.CLI/Program.cs b/Hippocrates.Journal-Service.CLI/Program.cs index 3751555..ca8db5e 100644 --- a/Hippocrates.Journal-Service.CLI/Program.cs +++ b/Hippocrates.Journal-Service.CLI/Program.cs @@ -1,2 +1,37 @@ // See https://aka.ms/new-console-template for more information -Console.WriteLine("Hello, World!"); +using Hippocrates.Journal.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +internal class Program { + private static void Main(string[] args) { + + var services = CreateServiceCollection(); + //var ProductService = services.GetService() ?? throw new Exception("Unable to locate a valid Product Logic module"); + //var OrderService = services.GetService() ?? throw new Exception("Unable to locate a valid Order Logic module"); + + static IServiceProvider CreateServiceCollection() { + var servicecollection = new ServiceCollection() + .AddDbContext(options => { + options.UseSqlite($"Data Source={DatabaseContext.GetSqliteDbPath()}"); + }) + //.AddSingleton() + //.AddSingleton() + //.AddSingleton() + //.AddSingleton() + .AddLogging(options => { + options.AddDebug(); + options.SetMinimumLevel(LogLevel.Error); + options.AddSimpleConsole(options => { + options.SingleLine = true; + options.TimestampFormat = "HH:mm:ss.fff "; + options.ColorBehavior = Microsoft.Extensions.Logging.Console.LoggerColorBehavior.Enabled; + }); + }); + + + return servicecollection.BuildServiceProvider(); + } + } +} \ No newline at end of file diff --git a/Hippocrates.Journal-WebAPI/Hippocrates.Journal.WebAPI.csproj b/Hippocrates.Journal-WebAPI/Hippocrates.Journal.WebAPI.csproj index 444bf18..7ed8472 100644 --- a/Hippocrates.Journal-WebAPI/Hippocrates.Journal.WebAPI.csproj +++ b/Hippocrates.Journal-WebAPI/Hippocrates.Journal.WebAPI.csproj @@ -10,6 +10,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Hippocrates.Journal-WebAPI/Program.cs b/Hippocrates.Journal-WebAPI/Program.cs index 3229d20..607f356 100644 --- a/Hippocrates.Journal-WebAPI/Program.cs +++ b/Hippocrates.Journal-WebAPI/Program.cs @@ -1,24 +1,28 @@ -var builder = WebApplication.CreateBuilder(args); +internal class Program { + private static void Main(string[] args) { + var builder = WebApplication.CreateBuilder(args); -// Add services to the container. + // Add services to the container. -builder.Services.AddControllers(); -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); + builder.Services.AddControllers(); + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(); -var app = builder.Build(); + var app = builder.Build(); -// Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) { - app.UseSwagger(); - app.UseSwaggerUI(); -} + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) { + app.UseSwagger(); + app.UseSwaggerUI(); + } -app.UseHttpsRedirection(); + app.UseHttpsRedirection(); -app.UseAuthorization(); + app.UseAuthorization(); -app.MapControllers(); + app.MapControllers(); -app.Run(); + app.Run(); + } +} \ No newline at end of file diff --git a/Hippocrates.Journal.Data/BaseRepository.cs b/Hippocrates.Journal.Data/BaseRepository.cs new file mode 100644 index 0000000..36db130 --- /dev/null +++ b/Hippocrates.Journal.Data/BaseRepository.cs @@ -0,0 +1,43 @@ +using Hippocrates.Journal.DomainEntities; +using Microsoft.EntityFrameworkCore; + +namespace Hippocrates.Journal.Data { + public abstract class BaseRepository(IDatabaseContext db, DbSet table) where T : BaseEntity{ + private readonly IDatabaseContext db = db; + private readonly DbSet table = table; + public async Task AddAsync(T entity) { + var row = await table.AddAsync(entity).ConfigureAwait(false); + await db.SaveChangesAsync().ConfigureAwait(false); + return row.Entity; + } + public async Task RemoveAsync(T entity) { + table.Remove(entity); + await db.SaveChangesAsync().ConfigureAwait(false); + } + public async Task GetAsync(int id) { + return await table.FirstOrDefaultAsync(t => t.Id == id).ConfigureAwait(false); + } + + public async Task GetAsync(Guid resourceId) { + return await table.FirstOrDefaultAsync(t => t.ResourceId == resourceId).ConfigureAwait(false); + } + + //TODO: can this handle created date and lastmodified + //date and prevent updates to id and resourceid + // and then call a delegate? -- look and see how cortside handles this + public abstract Task UpdateAsync(T entityUpdate); + //public override Task UpdateAsync(Event entityUpdate) { + //ArgumentNullException.ThrowIfNull(nameof(orderUpdate)); + //var existingOrder = await db.Orders.FindAsync(orderUpdate.OrderId).ConfigureAwait(false) + // ?? throw new InvalidOperationException($"Order Id {orderUpdate.OrderId} not found"); + + //existingOrder.OrderDate = orderUpdate.OrderDate; + //// TODO: this is actually bad and will likely leave orphaned OrderProducts + //// in the database + //existingOrder.OrderProducts = orderUpdate.OrderProducts; + //existingOrder.LastUpdatedDate = DateTime.Now; + //await db.SaveChangesAsync().ConfigureAwait(false); + //throw new NotImplementedException(); + //} + } +} diff --git a/Hippocrates.Journal.Data/DatabaseContext.cs b/Hippocrates.Journal.Data/DatabaseContext.cs index b29d488..a341431 100644 --- a/Hippocrates.Journal.Data/DatabaseContext.cs +++ b/Hippocrates.Journal.Data/DatabaseContext.cs @@ -1,22 +1,40 @@ using Hippocrates.Journal.DomainEntities; using Microsoft.EntityFrameworkCore; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Hippocrates.Journal.Data { - public class DatabaseContext { - public DbSet Symptoms { get; set; } - public DbSet Events { get; set; } + public class DatabaseContext : DbContext, IDatabaseContext { + public DbSet Symptoms { get; set; } = null!; + public DbSet Intensities { get; set; } = null!; + public DbSet Events { get; set; } = null!; + public DbSet EventTypes { get; set; } = null!; + public DbSet EventSymptoms { get; set; } = null!; + public DatabaseContext() { - // needed by ef clit tools + // needed by EF cli tools } - public DatabaseContext() { - + public DatabaseContext(DbContextOptions options) : base(options) { + // needed to properly inject DBContext at runtime + + } + + protected override void OnConfiguring(DbContextOptionsBuilder options) { + if (!options.IsConfigured) { + options.UseSqlite($"Data Source={GetSqliteDbPath()}"); + } + } + + public static string GetSqliteDbPath() { + // The following configures EF to create a Sqlite database file in the + // special "local" folder for your platform. + var folder = Environment.SpecialFolder.LocalApplicationData; + var path = Environment.GetFolderPath(folder); + return Path.Join(path, "PetShop.db"); + } + + public Task SaveChangesAsync() { + return base.SaveChangesAsync(); } } } diff --git a/Hippocrates.Journal.Data/EventRepository.cs b/Hippocrates.Journal.Data/EventRepository.cs index e1987ff..fa0956f 100644 --- a/Hippocrates.Journal.Data/EventRepository.cs +++ b/Hippocrates.Journal.Data/EventRepository.cs @@ -1,5 +1,19 @@ -namespace Hippocrates.Journal.Data { - public class EventRepository { +using Hippocrates.Journal.DomainEntities; +namespace Hippocrates.Journal.Data { + public class EventRepository(IDatabaseContext db) : BaseRepository(db, db.Events) { + public override Task UpdateAsync(Event entityUpdate) { + //ArgumentNullException.ThrowIfNull(nameof(orderUpdate)); + //var existingOrder = await db.Orders.FindAsync(orderUpdate.OrderId).ConfigureAwait(false) + // ?? throw new InvalidOperationException($"Order Id {orderUpdate.OrderId} not found"); + + //existingOrder.OrderDate = orderUpdate.OrderDate; + //// TODO: this is actually bad and will likely leave orphaned OrderProducts + //// in the database + //existingOrder.OrderProducts = orderUpdate.OrderProducts; + //existingOrder.LastUpdatedDate = DateTime.Now; + //await db.SaveChangesAsync().ConfigureAwait(false); + throw new NotImplementedException(); + } } } diff --git a/Hippocrates.Journal.Data/EventSymptomRepository.cs b/Hippocrates.Journal.Data/EventSymptomRepository.cs new file mode 100644 index 0000000..6a1fcca --- /dev/null +++ b/Hippocrates.Journal.Data/EventSymptomRepository.cs @@ -0,0 +1,6 @@ +using Hippocrates.Journal.DomainEntities; + +namespace Hippocrates.Journal.Data { + public class EventSymptomRepository(IDatabaseContext db) : BaseRepository(db, db.Symptoms) { + } +} diff --git a/Hippocrates.Journal.Data/EventTypeRepository.cs b/Hippocrates.Journal.Data/EventTypeRepository.cs new file mode 100644 index 0000000..56a3439 --- /dev/null +++ b/Hippocrates.Journal.Data/EventTypeRepository.cs @@ -0,0 +1,6 @@ +using Hippocrates.Journal.DomainEntities; + +namespace Hippocrates.Journal.Data { + public class EventTypeRepository(IDatabaseContext db) : BaseRepository(db, db.Symptoms) { + } +} diff --git a/Hippocrates.Journal.Data/Hippocrates.Journal.Data.csproj b/Hippocrates.Journal.Data/Hippocrates.Journal.Data.csproj index 01ef1e8..00633f3 100644 --- a/Hippocrates.Journal.Data/Hippocrates.Journal.Data.csproj +++ b/Hippocrates.Journal.Data/Hippocrates.Journal.Data.csproj @@ -7,6 +7,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Hippocrates.Journal.Data/IDatabaseContext.cs b/Hippocrates.Journal.Data/IDatabaseContext.cs new file mode 100644 index 0000000..20fef92 --- /dev/null +++ b/Hippocrates.Journal.Data/IDatabaseContext.cs @@ -0,0 +1,14 @@ +using Hippocrates.Journal.DomainEntities; +using Microsoft.EntityFrameworkCore; + +namespace Hippocrates.Journal.Data { + public interface IDatabaseContext { + DbSet Symptoms { get; set; } + DbSet Intensities { get; set; } + DbSet Events { get; set; } + DbSet EventTypes { get; set; } + DbSet EventSymptoms { get; set; } + + Task SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/Hippocrates.Journal.Data/IntensityReposity.cs b/Hippocrates.Journal.Data/IntensityReposity.cs new file mode 100644 index 0000000..306d873 --- /dev/null +++ b/Hippocrates.Journal.Data/IntensityReposity.cs @@ -0,0 +1,6 @@ +using Hippocrates.Journal.DomainEntities; + +namespace Hippocrates.Journal.Data { + public class IntensityReposity(IDatabaseContext db) : BaseRepository(db, db.Symptoms) { + } +} diff --git a/Hippocrates.Journal.Data/SymptomRepository.cs b/Hippocrates.Journal.Data/SymptomRepository.cs index 6ca7844..1c90986 100644 --- a/Hippocrates.Journal.Data/SymptomRepository.cs +++ b/Hippocrates.Journal.Data/SymptomRepository.cs @@ -1,10 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using Hippocrates.Journal.DomainEntities; namespace Hippocrates.Journal.Data { - internal class SymptomRepository { + public class SymptomRepository(IDatabaseContext db) : BaseRepository(db, db.Symptoms) { + } } From b74ec312690ebbe89281ddc9c19bacb65cf34f1a Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Sun, 13 Apr 2025 17:04:42 -0400 Subject: [PATCH 06/34] update repositories, entities need another analysis to make sure things work the way we want. almost ready to start adding entities on the CLI --- Hippocrates.Journal-DomainEntities/Event.cs | 2 +- .../Intensity.cs | 3 ++ Hippocrates.Journal-DomainEntities/Symptom.cs | 4 +- Hippocrates.Journal.Data/BaseRepository.cs | 47 ++++++++++++------- Hippocrates.Journal.Data/DatabaseContext.cs | 2 +- Hippocrates.Journal.Data/EventRepository.cs | 17 ++----- .../EventSymptomRepository.cs | 10 +++- .../EventTypeRepository.cs | 8 +++- Hippocrates.Journal.Data/IDatabaseContext.cs | 6 +-- .../IntensityRepository.cs | 13 +++++ Hippocrates.Journal.Data/IntensityReposity.cs | 6 --- Hippocrates.Journal.Data/SymptomRepository.cs | 7 ++- 12 files changed, 80 insertions(+), 45 deletions(-) create mode 100644 Hippocrates.Journal.Data/IntensityRepository.cs delete mode 100644 Hippocrates.Journal.Data/IntensityReposity.cs diff --git a/Hippocrates.Journal-DomainEntities/Event.cs b/Hippocrates.Journal-DomainEntities/Event.cs index f2138ab..3c00f22 100644 --- a/Hippocrates.Journal-DomainEntities/Event.cs +++ b/Hippocrates.Journal-DomainEntities/Event.cs @@ -4,7 +4,7 @@ namespace Hippocrates.Journal.DomainEntities { public class Event : BaseEntity { [Key] public int EventId { get { return Id; } set { Id = value; } } [Required] public Guid EventResourceId { get { return ResourceId; } set { ResourceId = value; } } - [Required] public DateTime Timestamp { get; set; } = DateTime.Now; + [Required] public DateTime EventTime { get; set; } = DateTime.Now; [MaxLength(500)] public string? Description { get; set; } } } diff --git a/Hippocrates.Journal-DomainEntities/Intensity.cs b/Hippocrates.Journal-DomainEntities/Intensity.cs index f205241..ecc6713 100644 --- a/Hippocrates.Journal-DomainEntities/Intensity.cs +++ b/Hippocrates.Journal-DomainEntities/Intensity.cs @@ -1,5 +1,6 @@ using Hippocrates.Journal.DomainEntities.Enums; using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; namespace Hippocrates.Journal.DomainEntities { public class Intensity :BaseEntity { @@ -9,6 +10,8 @@ public class Intensity :BaseEntity { [Required] public required int Level { get; set; } [MaxLength(500)] public string? Description { get; set; } [Required] public required SortType DefaultSortType { get; set; } + [ForeignKey(nameof(SymptomId))] + [Required] public required int SymptomId { get; set; } } } diff --git a/Hippocrates.Journal-DomainEntities/Symptom.cs b/Hippocrates.Journal-DomainEntities/Symptom.cs index adadbf0..f6c4da6 100644 --- a/Hippocrates.Journal-DomainEntities/Symptom.cs +++ b/Hippocrates.Journal-DomainEntities/Symptom.cs @@ -4,7 +4,7 @@ namespace Hippocrates.Journal.DomainEntities { public class Symptom : BaseEntity { [Key] public int SymptomId { get { return Id; } set { Id = value; } } [Required] public Guid SymptomResourceId { get { return ResourceId; } set { ResourceId = value; } } - [Required] public required string SymptomName { get; set; } - [MaxLength(500)] public string? SymptomDescription { get; set; } + [Required] public required string Name { get; set; } + [MaxLength(500)] public string? Description { get; set; } } } diff --git a/Hippocrates.Journal.Data/BaseRepository.cs b/Hippocrates.Journal.Data/BaseRepository.cs index 36db130..5f3331b 100644 --- a/Hippocrates.Journal.Data/BaseRepository.cs +++ b/Hippocrates.Journal.Data/BaseRepository.cs @@ -15,29 +15,42 @@ public async Task RemoveAsync(T entity) { await db.SaveChangesAsync().ConfigureAwait(false); } public async Task GetAsync(int id) { - return await table.FirstOrDefaultAsync(t => t.Id == id).ConfigureAwait(false); + return await table.FindAsync(id).ConfigureAwait(false); } public async Task GetAsync(Guid resourceId) { return await table.FirstOrDefaultAsync(t => t.ResourceId == resourceId).ConfigureAwait(false); } - //TODO: can this handle created date and lastmodified - //date and prevent updates to id and resourceid - // and then call a delegate? -- look and see how cortside handles this - public abstract Task UpdateAsync(T entityUpdate); - //public override Task UpdateAsync(Event entityUpdate) { - //ArgumentNullException.ThrowIfNull(nameof(orderUpdate)); - //var existingOrder = await db.Orders.FindAsync(orderUpdate.OrderId).ConfigureAwait(false) - // ?? throw new InvalidOperationException($"Order Id {orderUpdate.OrderId} not found"); - //existingOrder.OrderDate = orderUpdate.OrderDate; - //// TODO: this is actually bad and will likely leave orphaned OrderProducts - //// in the database - //existingOrder.OrderProducts = orderUpdate.OrderProducts; - //existingOrder.LastUpdatedDate = DateTime.Now; - //await db.SaveChangesAsync().ConfigureAwait(false); - //throw new NotImplementedException(); - //} + /// + /// Copy values of one entity to another, to be implemented by descendants. + /// Recommend making use of automapper or some such. Note that CopyEntity should NOT + /// set any of the BaseEntity values. Especially Id and ResourceId. + /// + /// + /// + protected abstract void CopyEntity(T destinationEntity, T sourceEntity); + + public async Task UpdateAsync(T destinationEntity) { + ArgumentException.ThrowIfNullOrEmpty(nameof(destinationEntity)); + var existingEntity = await GetAsync(destinationEntity.Id) + ?? await GetAsync(destinationEntity.ResourceId); + if (existingEntity == null) { + return await AddAsync(destinationEntity).ConfigureAwait(false); + } + + int id = existingEntity.Id; + Guid resourceId = existingEntity.ResourceId; + CopyEntity(destinationEntity, existingEntity); + + // set updated date and ensure CopyEntity implementations don't reset id or resource id + existingEntity.UpdatedDate = DateTime.UtcNow; + existingEntity.Id = id; + existingEntity.ResourceId = resourceId; + + await db.SaveChangesAsync().ConfigureAwait(false); + return existingEntity; + } } } diff --git a/Hippocrates.Journal.Data/DatabaseContext.cs b/Hippocrates.Journal.Data/DatabaseContext.cs index a341431..7b4a3a5 100644 --- a/Hippocrates.Journal.Data/DatabaseContext.cs +++ b/Hippocrates.Journal.Data/DatabaseContext.cs @@ -5,7 +5,7 @@ namespace Hippocrates.Journal.Data { public class DatabaseContext : DbContext, IDatabaseContext { public DbSet Symptoms { get; set; } = null!; - public DbSet Intensities { get; set; } = null!; + public DbSet Intensities { get; set; } = null!; public DbSet Events { get; set; } = null!; public DbSet EventTypes { get; set; } = null!; public DbSet EventSymptoms { get; set; } = null!; diff --git a/Hippocrates.Journal.Data/EventRepository.cs b/Hippocrates.Journal.Data/EventRepository.cs index fa0956f..61d8d07 100644 --- a/Hippocrates.Journal.Data/EventRepository.cs +++ b/Hippocrates.Journal.Data/EventRepository.cs @@ -2,18 +2,11 @@ namespace Hippocrates.Journal.Data { public class EventRepository(IDatabaseContext db) : BaseRepository(db, db.Events) { - public override Task UpdateAsync(Event entityUpdate) { - //ArgumentNullException.ThrowIfNull(nameof(orderUpdate)); - //var existingOrder = await db.Orders.FindAsync(orderUpdate.OrderId).ConfigureAwait(false) - // ?? throw new InvalidOperationException($"Order Id {orderUpdate.OrderId} not found"); - - //existingOrder.OrderDate = orderUpdate.OrderDate; - //// TODO: this is actually bad and will likely leave orphaned OrderProducts - //// in the database - //existingOrder.OrderProducts = orderUpdate.OrderProducts; - //existingOrder.LastUpdatedDate = DateTime.Now; - //await db.SaveChangesAsync().ConfigureAwait(false); - throw new NotImplementedException(); + protected override void CopyEntity(Event destinationEntity, Event sourceEntity) { + //TODO: should these really call a copy method on the entity itself? + //TODO: could this be done with automapper or some such + destinationEntity.Description = sourceEntity.Description; + destinationEntity.EventTime = sourceEntity.EventTime; } } } diff --git a/Hippocrates.Journal.Data/EventSymptomRepository.cs b/Hippocrates.Journal.Data/EventSymptomRepository.cs index 6a1fcca..501a264 100644 --- a/Hippocrates.Journal.Data/EventSymptomRepository.cs +++ b/Hippocrates.Journal.Data/EventSymptomRepository.cs @@ -1,6 +1,14 @@ using Hippocrates.Journal.DomainEntities; namespace Hippocrates.Journal.Data { - public class EventSymptomRepository(IDatabaseContext db) : BaseRepository(db, db.Symptoms) { + public class EventSymptomRepository(IDatabaseContext db) : BaseRepository(db, db.EventSymptoms) { + protected override void CopyEntity(EventSymptom destinationEntity, EventSymptom sourceEntity) { + //TODO: should these really call a copy method on the entity itself? + //TODO: could this be done with automapper or some such + destinationEntity.Event = sourceEntity.Event; + destinationEntity.Symptom = sourceEntity.Symptom; + destinationEntity.Intensity = sourceEntity.Intensity; + destinationEntity.Notes = sourceEntity.Notes; + } } } diff --git a/Hippocrates.Journal.Data/EventTypeRepository.cs b/Hippocrates.Journal.Data/EventTypeRepository.cs index 56a3439..672a00c 100644 --- a/Hippocrates.Journal.Data/EventTypeRepository.cs +++ b/Hippocrates.Journal.Data/EventTypeRepository.cs @@ -1,6 +1,12 @@ using Hippocrates.Journal.DomainEntities; namespace Hippocrates.Journal.Data { - public class EventTypeRepository(IDatabaseContext db) : BaseRepository(db, db.Symptoms) { + public class EventTypeRepository(IDatabaseContext db) : BaseRepository(db, db.EventTypes) { + protected override void CopyEntity(EventType destinationEntity, EventType sourceEntity) { + //TODO: should these really call a copy method on the entity itself? + //TODO: could this be done with automapper or some such + destinationEntity.Description = sourceEntity.Description; + destinationEntity.Name = sourceEntity.Name; + } } } diff --git a/Hippocrates.Journal.Data/IDatabaseContext.cs b/Hippocrates.Journal.Data/IDatabaseContext.cs index 20fef92..631845c 100644 --- a/Hippocrates.Journal.Data/IDatabaseContext.cs +++ b/Hippocrates.Journal.Data/IDatabaseContext.cs @@ -4,10 +4,10 @@ namespace Hippocrates.Journal.Data { public interface IDatabaseContext { DbSet Symptoms { get; set; } - DbSet Intensities { get; set; } + DbSet Intensities { get; set; } DbSet Events { get; set; } - DbSet EventTypes { get; set; } - DbSet EventSymptoms { get; set; } + DbSet EventTypes { get; set; } + DbSet EventSymptoms { get; set; } Task SaveChangesAsync(); } diff --git a/Hippocrates.Journal.Data/IntensityRepository.cs b/Hippocrates.Journal.Data/IntensityRepository.cs new file mode 100644 index 0000000..879771c --- /dev/null +++ b/Hippocrates.Journal.Data/IntensityRepository.cs @@ -0,0 +1,13 @@ +using Hippocrates.Journal.DomainEntities; + +namespace Hippocrates.Journal.Data { + public class IntensityRepository(IDatabaseContext db) : BaseRepository(db, db.Intensities) { + protected override void CopyEntity(Intensity destinationEntity, Intensity sourceEntity) { + //TODO: could this be done with automapper or some such + destinationEntity.Level = sourceEntity.Level; + destinationEntity.Name = sourceEntity.Name; + destinationEntity.Description = sourceEntity.Description; + destinationEntity.DefaultSortType = sourceEntity.DefaultSortType; + } + } +} diff --git a/Hippocrates.Journal.Data/IntensityReposity.cs b/Hippocrates.Journal.Data/IntensityReposity.cs deleted file mode 100644 index 306d873..0000000 --- a/Hippocrates.Journal.Data/IntensityReposity.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Hippocrates.Journal.DomainEntities; - -namespace Hippocrates.Journal.Data { - public class IntensityReposity(IDatabaseContext db) : BaseRepository(db, db.Symptoms) { - } -} diff --git a/Hippocrates.Journal.Data/SymptomRepository.cs b/Hippocrates.Journal.Data/SymptomRepository.cs index 1c90986..ed49645 100644 --- a/Hippocrates.Journal.Data/SymptomRepository.cs +++ b/Hippocrates.Journal.Data/SymptomRepository.cs @@ -2,6 +2,11 @@ namespace Hippocrates.Journal.Data { public class SymptomRepository(IDatabaseContext db) : BaseRepository(db, db.Symptoms) { - + protected override void CopyEntity(Symptom destinationEntity, Symptom sourceEntity) { + //TODO: should these really call a copy method on the entity itself? + //TODO: could this be done with automapper or some such + destinationEntity.Description = sourceEntity.Description; + destinationEntity.Name = sourceEntity.Name; + } } } From 61101a4f2b7e54b17dcbe0f22948fbb009ba5ebd Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Sun, 22 Jun 2025 16:50:18 -0400 Subject: [PATCH 07/34] rework of entities and repos --- Hippocrates.Journal.sln => EventJournal.sln | 8 ++--- .../BaseEntity.cs | 19 ++++++++++- Hippocrates.Journal-DomainEntities/Detail.cs | 17 ++++++++++ .../Enums/SortType.cs | 2 +- Hippocrates.Journal-DomainEntities/Event.cs | 22 +++++++++--- ...roj => EventJournal.DomainEntities.csproj} | 2 +- .../EventSymptom.cs | 12 ------- Hippocrates.Journal-DomainEntities/Symptom.cs | 10 ------ .../UserTypes/DetailType.cs | 16 +++++++++ .../{ => UserTypes}/EventType.cs | 17 +++++++--- .../{ => UserTypes}/Intensity.cs | 14 +++++--- ...nal.CLI.csproj => EventJournal.CLI.csproj} | 5 ++- Hippocrates.Journal-Service.CLI/Program.cs | 2 +- .../Controllers/WeatherForecastController.cs | 2 +- ...bAPI.csproj => EventJournal.WebAPI.csproj} | 0 .../EventJournal.WebAPI.http | 6 ++++ .../Hippocrates.Journal.WebAPI.http | 6 ---- Hippocrates.Journal-WebAPI/WeatherForecast.cs | 2 +- Hippocrates.Journal.Data/BaseRepository.cs | 34 +++++-------------- Hippocrates.Journal.Data/DatabaseContext.cs | 15 ++++---- Hippocrates.Journal.Data/DetailRepository.cs | 6 ++++ ...l.Data.csproj => EventJournal.Data.csproj} | 2 +- Hippocrates.Journal.Data/EventRepository.cs | 10 ++---- .../EventSymptomRepository.cs | 14 -------- .../EventTypeRepository.cs | 12 ------- Hippocrates.Journal.Data/IDatabaseContext.cs | 11 +++--- .../IntensityRepository.cs | 13 ------- Hippocrates.Journal.Data/SymptomRepository.cs | 12 ------- .../DetailTypeRepository.cs | 7 ++++ .../EventTypeRepository.cs | 6 ++++ .../IntensityRepository.cs | 8 +++++ README.md | 17 +++++----- 32 files changed, 169 insertions(+), 160 deletions(-) rename Hippocrates.Journal.sln => EventJournal.sln (73%) create mode 100644 Hippocrates.Journal-DomainEntities/Detail.cs rename Hippocrates.Journal-DomainEntities/{Hippocrates.Journal.DomainEntities.csproj => EventJournal.DomainEntities.csproj} (82%) delete mode 100644 Hippocrates.Journal-DomainEntities/EventSymptom.cs delete mode 100644 Hippocrates.Journal-DomainEntities/Symptom.cs create mode 100644 Hippocrates.Journal-DomainEntities/UserTypes/DetailType.cs rename Hippocrates.Journal-DomainEntities/{ => UserTypes}/EventType.cs (58%) rename Hippocrates.Journal-DomainEntities/{ => UserTypes}/Intensity.cs (60%) rename Hippocrates.Journal-Service.CLI/{Hippocrates.Journal.CLI.csproj => EventJournal.CLI.csproj} (82%) rename Hippocrates.Journal-WebAPI/{Hippocrates.Journal.WebAPI.csproj => EventJournal.WebAPI.csproj} (100%) create mode 100644 Hippocrates.Journal-WebAPI/EventJournal.WebAPI.http delete mode 100644 Hippocrates.Journal-WebAPI/Hippocrates.Journal.WebAPI.http create mode 100644 Hippocrates.Journal.Data/DetailRepository.cs rename Hippocrates.Journal.Data/{Hippocrates.Journal.Data.csproj => EventJournal.Data.csproj} (92%) delete mode 100644 Hippocrates.Journal.Data/EventSymptomRepository.cs delete mode 100644 Hippocrates.Journal.Data/EventTypeRepository.cs delete mode 100644 Hippocrates.Journal.Data/IntensityRepository.cs delete mode 100644 Hippocrates.Journal.Data/SymptomRepository.cs create mode 100644 Hippocrates.Journal.Data/UserTypeRepositories/DetailTypeRepository.cs create mode 100644 Hippocrates.Journal.Data/UserTypeRepositories/EventTypeRepository.cs create mode 100644 Hippocrates.Journal.Data/UserTypeRepositories/IntensityRepository.cs diff --git a/Hippocrates.Journal.sln b/EventJournal.sln similarity index 73% rename from Hippocrates.Journal.sln rename to EventJournal.sln index 55cc7e7..0ae955e 100644 --- a/Hippocrates.Journal.sln +++ b/EventJournal.sln @@ -3,9 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.12.35521.163 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hippocrates.Journal.CLI", "Hippocrates.Journal-Service.CLI\Hippocrates.Journal.CLI.csproj", "{D0E93737-A883-4020-9C99-E6C1D70D1237}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.CLI", "Hippocrates.Journal-Service.CLI\EventJournal.CLI.csproj", "{D0E93737-A883-4020-9C99-E6C1D70D1237}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hippocrates.Journal.DomainEntities", "Hippocrates.Journal-DomainEntities\Hippocrates.Journal.DomainEntities.csproj", "{2F884BE1-F88D-45F7-A3E0-480FCA94541A}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.DomainEntities", "Hippocrates.Journal-DomainEntities\EventJournal.DomainEntities.csproj", "{2F884BE1-F88D-45F7-A3E0-480FCA94541A}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{994FEE9A-A85B-4AEC-894B-CD7FEDF3FC09}" ProjectSection(SolutionItems) = preProject @@ -13,9 +13,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hippocrates.Journal.WebAPI", "Hippocrates.Journal-WebAPI\Hippocrates.Journal.WebAPI.csproj", "{AFC2B8DD-6CDE-450F-9181-93B2B67983D7}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.WebAPI", "Hippocrates.Journal-WebAPI\EventJournal.WebAPI.csproj", "{AFC2B8DD-6CDE-450F-9181-93B2B67983D7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hippocrates.Journal.Data", "Hippocrates.Journal.Data\Hippocrates.Journal.Data.csproj", "{CD1F9E2E-4F39-4E6C-B018-B3BF858D57A5}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.Data", "Hippocrates.Journal.Data\EventJournal.Data.csproj", "{CD1F9E2E-4F39-4E6C-B018-B3BF858D57A5}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/Hippocrates.Journal-DomainEntities/BaseEntity.cs b/Hippocrates.Journal-DomainEntities/BaseEntity.cs index 8847196..c706c20 100644 --- a/Hippocrates.Journal-DomainEntities/BaseEntity.cs +++ b/Hippocrates.Journal-DomainEntities/BaseEntity.cs @@ -1,7 +1,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -namespace Hippocrates.Journal.DomainEntities { +namespace EventJournal.DomainEntities { public abstract class BaseEntity { [Required] public DateTime CreatedDate { get; set; } = DateTime.Now; [Required] public DateTime UpdatedDate { get; set; } = DateTime.MinValue; @@ -9,5 +9,22 @@ public abstract class BaseEntity { [Required] public int Id { get; set; } [NotMapped] [Required] public Guid ResourceId { get; set; } + + /// + /// This method is predominantly for updating an entity based on the values in another entity. + /// Copy user values (ids and audit info (created, last modified, etc) should NOT be copied! + /// + /// + /// + internal abstract void CopyUserValues(T source); + } + + public static partial class EntityHelpers { + public static T UpdateEntity(this T destination, T source) where T : BaseEntity { + destination.CopyUserValues(source); + destination.CreatedDate = source.CreatedDate; + destination.UpdatedDate = DateTime.UtcNow; + return destination; + } } } diff --git a/Hippocrates.Journal-DomainEntities/Detail.cs b/Hippocrates.Journal-DomainEntities/Detail.cs new file mode 100644 index 0000000..656160f --- /dev/null +++ b/Hippocrates.Journal-DomainEntities/Detail.cs @@ -0,0 +1,17 @@ +using EventJournal.DomainEntities.UserTypes; +using System.ComponentModel.DataAnnotations; + +namespace EventJournal.DomainEntities { + public class Detail : BaseEntity{ + [Key] public int DetailId { get { return Id; } set { Id = value; } } + [Required] public Guid DetailResourceId { get { return ResourceId; } set { ResourceId = value; } } + [Required] public virtual required Event Event { get; set; } + [Required] public virtual required DetailType DetailType { get; set; } + [Required] public virtual required Intensity Intensity { get; set; } + [MaxLength(512)] public string? Notes { get; set; } + + internal override void CopyUserValues(T source) { + throw new NotImplementedException(); + } + } +} diff --git a/Hippocrates.Journal-DomainEntities/Enums/SortType.cs b/Hippocrates.Journal-DomainEntities/Enums/SortType.cs index 86e1b38..c7bd6cd 100644 --- a/Hippocrates.Journal-DomainEntities/Enums/SortType.cs +++ b/Hippocrates.Journal-DomainEntities/Enums/SortType.cs @@ -1,4 +1,4 @@ -namespace Hippocrates.Journal.DomainEntities.Enums { +namespace EventJournal.DomainEntities.Enums { public enum SortType { Custom, Ascending, Descending } diff --git a/Hippocrates.Journal-DomainEntities/Event.cs b/Hippocrates.Journal-DomainEntities/Event.cs index 3c00f22..ab23d9d 100644 --- a/Hippocrates.Journal-DomainEntities/Event.cs +++ b/Hippocrates.Journal-DomainEntities/Event.cs @@ -1,10 +1,24 @@ -using System.ComponentModel.DataAnnotations; +using EventJournal.DomainEntities.UserTypes; +using System.ComponentModel.DataAnnotations; -namespace Hippocrates.Journal.DomainEntities { +namespace EventJournal.DomainEntities { public class Event : BaseEntity { [Key] public int EventId { get { return Id; } set { Id = value; } } [Required] public Guid EventResourceId { get { return ResourceId; } set { ResourceId = value; } } - [Required] public DateTime EventTime { get; set; } = DateTime.Now; + [Required] public EventType Type { get; set; } = EventType.GetDefaultEventType(); + [Required] public DateTime StartTime { get; set; } = DateTime.Now; + public DateTime? EndTime { get; set; } = null; [MaxLength(500)] public string? Description { get; set; } + + public IEnumerable Details { get; set; } = []; + + internal override void CopyUserValues(T source) { + var soruceEvent = source as Event ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(Event)}"); + Type = soruceEvent.Type; + StartTime = soruceEvent.StartTime; + EndTime = soruceEvent.EndTime; + Description = soruceEvent.Description; + Details = soruceEvent.Details; + } } -} +} \ No newline at end of file diff --git a/Hippocrates.Journal-DomainEntities/Hippocrates.Journal.DomainEntities.csproj b/Hippocrates.Journal-DomainEntities/EventJournal.DomainEntities.csproj similarity index 82% rename from Hippocrates.Journal-DomainEntities/Hippocrates.Journal.DomainEntities.csproj rename to Hippocrates.Journal-DomainEntities/EventJournal.DomainEntities.csproj index 623aa14..4e0e8d4 100644 --- a/Hippocrates.Journal-DomainEntities/Hippocrates.Journal.DomainEntities.csproj +++ b/Hippocrates.Journal-DomainEntities/EventJournal.DomainEntities.csproj @@ -2,7 +2,7 @@ net8.0 - Hippocrates.Journal.DomainEntities + EventJournal.DomainEntities enable enable diff --git a/Hippocrates.Journal-DomainEntities/EventSymptom.cs b/Hippocrates.Journal-DomainEntities/EventSymptom.cs deleted file mode 100644 index b4a030d..0000000 --- a/Hippocrates.Journal-DomainEntities/EventSymptom.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Hippocrates.Journal.DomainEntities { - public class EventSymptom : BaseEntity{ - [Key] public int EventSymptomId { get { return Id; } set { Id = value; } } - [Required] public Guid EventSymptomResourceId { get { return ResourceId; } set { ResourceId = value; } } - [Required] public virtual required Event Event { get; set; } - [Required] public virtual required Symptom Symptom { get; set; } - [Required] public virtual required Intensity Intensity { get; set; } - [MaxLength(512)] public string? Notes { get; set; } - } -} diff --git a/Hippocrates.Journal-DomainEntities/Symptom.cs b/Hippocrates.Journal-DomainEntities/Symptom.cs deleted file mode 100644 index f6c4da6..0000000 --- a/Hippocrates.Journal-DomainEntities/Symptom.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Hippocrates.Journal.DomainEntities { - public class Symptom : BaseEntity { - [Key] public int SymptomId { get { return Id; } set { Id = value; } } - [Required] public Guid SymptomResourceId { get { return ResourceId; } set { ResourceId = value; } } - [Required] public required string Name { get; set; } - [MaxLength(500)] public string? Description { get; set; } - } -} diff --git a/Hippocrates.Journal-DomainEntities/UserTypes/DetailType.cs b/Hippocrates.Journal-DomainEntities/UserTypes/DetailType.cs new file mode 100644 index 0000000..70b3530 --- /dev/null +++ b/Hippocrates.Journal-DomainEntities/UserTypes/DetailType.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; + +namespace EventJournal.DomainEntities.UserTypes { + public class DetailType : BaseEntity { + [Key] public int DetailTypeId { get { return Id; } set { Id = value; } } + [Required] public Guid DetailTypeResourceId { get { return ResourceId; } set { ResourceId = value; } } + [Required] public required string Name { get; set; } + [MaxLength(500)] public string? Description { get; set; } + + public IEnumerable Intensities { get; set; } = []; + + internal override void CopyUserValues(T source) { + throw new NotImplementedException(); + } + } +} diff --git a/Hippocrates.Journal-DomainEntities/EventType.cs b/Hippocrates.Journal-DomainEntities/UserTypes/EventType.cs similarity index 58% rename from Hippocrates.Journal-DomainEntities/EventType.cs rename to Hippocrates.Journal-DomainEntities/UserTypes/EventType.cs index 7dbb565..f374730 100644 --- a/Hippocrates.Journal-DomainEntities/EventType.cs +++ b/Hippocrates.Journal-DomainEntities/UserTypes/EventType.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace Hippocrates.Journal.DomainEntities { +namespace EventJournal.DomainEntities.UserTypes { public class EventType : BaseEntity { [Key] public int EventTypeId { get { return Id; } set { Id = value; } } [Required] public Guid EventTypeResourceId { get { return ResourceId; } set { ResourceId = value; } } @@ -10,11 +10,20 @@ public class EventType : BaseEntity { //TODO: this code really belongs in a service or maybe repo public static IEnumerable DefaultEventTypes() { return [ - new EventType { EventTypeResourceId = Guid.NewGuid(), Name = "Random Event", Description = "Use for tracking random things like onset of pain, headache, or whatever that isn't directly associated with a specific even type."}, + GetDefaultEventType(), new EventType { EventTypeResourceId = Guid.NewGuid(), Name = "Exercise" }, - new EventType { EventTypeResourceId = Guid.NewGuid(), Name = "Bathroom visit" }, - new EventType { EventTypeResourceId = Guid.NewGuid(), Name = "Meal" }, + new EventType { EventTypeResourceId = Guid.NewGuid(), Name = "Bathroom Visit" }, + new EventType { EventTypeResourceId = Guid.NewGuid(), Name = "Food Consumption" }, + new EventType { EventTypeResourceId = Guid.NewGuid(), Name = "WeighIn" }, ]; } + + public static EventType GetDefaultEventType() { + return new EventType { EventTypeResourceId = Guid.NewGuid(), Name = "Random Event", Description = "Use for tracking random things like onset of pain, headache, or whatever that isn't directly associated with a specific even type." }; + } + + internal override void CopyUserValues(T source) { + throw new NotImplementedException(); + } } } diff --git a/Hippocrates.Journal-DomainEntities/Intensity.cs b/Hippocrates.Journal-DomainEntities/UserTypes/Intensity.cs similarity index 60% rename from Hippocrates.Journal-DomainEntities/Intensity.cs rename to Hippocrates.Journal-DomainEntities/UserTypes/Intensity.cs index ecc6713..d197d89 100644 --- a/Hippocrates.Journal-DomainEntities/Intensity.cs +++ b/Hippocrates.Journal-DomainEntities/UserTypes/Intensity.cs @@ -1,17 +1,21 @@ -using Hippocrates.Journal.DomainEntities.Enums; +using EventJournal.DomainEntities.Enums; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -namespace Hippocrates.Journal.DomainEntities { - public class Intensity :BaseEntity { +namespace EventJournal.DomainEntities.UserTypes { + public class Intensity : BaseEntity { [Key] public int IntensityId { get { return Id; } set { Id = value; } } [Required] public Guid IntensityResourceId { get { return ResourceId; } set { ResourceId = value; } } [Required, MaxLength(50)] public required string Name { get; set; } [Required] public required int Level { get; set; } [MaxLength(500)] public string? Description { get; set; } [Required] public required SortType DefaultSortType { get; set; } - [ForeignKey(nameof(SymptomId))] - [Required] public required int SymptomId { get; set; } + [ForeignKey(nameof(DetailTypeId))] + [Required] public required int DetailTypeId { get; set; } + + internal override void CopyUserValues(T source) { + throw new NotImplementedException(); + } } } diff --git a/Hippocrates.Journal-Service.CLI/Hippocrates.Journal.CLI.csproj b/Hippocrates.Journal-Service.CLI/EventJournal.CLI.csproj similarity index 82% rename from Hippocrates.Journal-Service.CLI/Hippocrates.Journal.CLI.csproj rename to Hippocrates.Journal-Service.CLI/EventJournal.CLI.csproj index 1d931e4..69ce840 100644 --- a/Hippocrates.Journal-Service.CLI/Hippocrates.Journal.CLI.csproj +++ b/Hippocrates.Journal-Service.CLI/EventJournal.CLI.csproj @@ -3,7 +3,7 @@ Exe net8.0 - Hippocrates.Journal.CLI + EventJournal.CLI enable enable @@ -18,7 +18,6 @@ - + - diff --git a/Hippocrates.Journal-Service.CLI/Program.cs b/Hippocrates.Journal-Service.CLI/Program.cs index ca8db5e..fd078b8 100644 --- a/Hippocrates.Journal-Service.CLI/Program.cs +++ b/Hippocrates.Journal-Service.CLI/Program.cs @@ -1,5 +1,5 @@ // See https://aka.ms/new-console-template for more information -using Hippocrates.Journal.Data; +using EventJournal.Data; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; diff --git a/Hippocrates.Journal-WebAPI/Controllers/WeatherForecastController.cs b/Hippocrates.Journal-WebAPI/Controllers/WeatherForecastController.cs index aa8f286..34af65b 100644 --- a/Hippocrates.Journal-WebAPI/Controllers/WeatherForecastController.cs +++ b/Hippocrates.Journal-WebAPI/Controllers/WeatherForecastController.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Mvc; -namespace Hippocrates.Journal.WebAPI.Controllers { +namespace EventJournal.WebAPI.Controllers { [ApiController] [Route("[controller]")] public class WeatherForecastController : ControllerBase { diff --git a/Hippocrates.Journal-WebAPI/Hippocrates.Journal.WebAPI.csproj b/Hippocrates.Journal-WebAPI/EventJournal.WebAPI.csproj similarity index 100% rename from Hippocrates.Journal-WebAPI/Hippocrates.Journal.WebAPI.csproj rename to Hippocrates.Journal-WebAPI/EventJournal.WebAPI.csproj diff --git a/Hippocrates.Journal-WebAPI/EventJournal.WebAPI.http b/Hippocrates.Journal-WebAPI/EventJournal.WebAPI.http new file mode 100644 index 0000000..3ce565b --- /dev/null +++ b/Hippocrates.Journal-WebAPI/EventJournal.WebAPI.http @@ -0,0 +1,6 @@ +@EventJournal.WebAPI_HostAddress = http://localhost:5256 + +GET {{EventJournal.WebAPI_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/Hippocrates.Journal-WebAPI/Hippocrates.Journal.WebAPI.http b/Hippocrates.Journal-WebAPI/Hippocrates.Journal.WebAPI.http deleted file mode 100644 index 0352af9..0000000 --- a/Hippocrates.Journal-WebAPI/Hippocrates.Journal.WebAPI.http +++ /dev/null @@ -1,6 +0,0 @@ -@Hippocrates.Journal.WebAPI_HostAddress = http://localhost:5256 - -GET {{Hippocrates.Journal.WebAPI_HostAddress}}/weatherforecast/ -Accept: application/json - -### diff --git a/Hippocrates.Journal-WebAPI/WeatherForecast.cs b/Hippocrates.Journal-WebAPI/WeatherForecast.cs index 2bd2c71..f9177ed 100644 --- a/Hippocrates.Journal-WebAPI/WeatherForecast.cs +++ b/Hippocrates.Journal-WebAPI/WeatherForecast.cs @@ -1,4 +1,4 @@ -namespace Hippocrates.Journal.WebAPI { +namespace EventJournal.WebAPI { public class WeatherForecast { public DateOnly Date { get; set; } diff --git a/Hippocrates.Journal.Data/BaseRepository.cs b/Hippocrates.Journal.Data/BaseRepository.cs index 5f3331b..e087f4d 100644 --- a/Hippocrates.Journal.Data/BaseRepository.cs +++ b/Hippocrates.Journal.Data/BaseRepository.cs @@ -1,7 +1,7 @@ -using Hippocrates.Journal.DomainEntities; +using EventJournal.DomainEntities; using Microsoft.EntityFrameworkCore; -namespace Hippocrates.Journal.Data { +namespace EventJournal.Data { public abstract class BaseRepository(IDatabaseContext db, DbSet table) where T : BaseEntity{ private readonly IDatabaseContext db = db; private readonly DbSet table = table; @@ -22,32 +22,16 @@ public async Task RemoveAsync(T entity) { return await table.FirstOrDefaultAsync(t => t.ResourceId == resourceId).ConfigureAwait(false); } - - /// - /// Copy values of one entity to another, to be implemented by descendants. - /// Recommend making use of automapper or some such. Note that CopyEntity should NOT - /// set any of the BaseEntity values. Especially Id and ResourceId. - /// - /// - /// - protected abstract void CopyEntity(T destinationEntity, T sourceEntity); - - public async Task UpdateAsync(T destinationEntity) { - ArgumentException.ThrowIfNullOrEmpty(nameof(destinationEntity)); - var existingEntity = await GetAsync(destinationEntity.Id) - ?? await GetAsync(destinationEntity.ResourceId); + // TODO: this seems like it could/should be an extension method + public async Task SaveEntity(T source) { + ArgumentException.ThrowIfNullOrEmpty(nameof(source)); + var existingEntity = await GetAsync(source.Id) + ?? await GetAsync(source.ResourceId); if (existingEntity == null) { - return await AddAsync(destinationEntity).ConfigureAwait(false); + return await AddAsync(source).ConfigureAwait(false); } - int id = existingEntity.Id; - Guid resourceId = existingEntity.ResourceId; - CopyEntity(destinationEntity, existingEntity); - - // set updated date and ensure CopyEntity implementations don't reset id or resource id - existingEntity.UpdatedDate = DateTime.UtcNow; - existingEntity.Id = id; - existingEntity.ResourceId = resourceId; + existingEntity.UpdateEntity(source); await db.SaveChangesAsync().ConfigureAwait(false); return existingEntity; diff --git a/Hippocrates.Journal.Data/DatabaseContext.cs b/Hippocrates.Journal.Data/DatabaseContext.cs index 7b4a3a5..220c5e3 100644 --- a/Hippocrates.Journal.Data/DatabaseContext.cs +++ b/Hippocrates.Journal.Data/DatabaseContext.cs @@ -1,14 +1,15 @@ -using Hippocrates.Journal.DomainEntities; +using EventJournal.DomainEntities; +using EventJournal.DomainEntities.UserTypes; using Microsoft.EntityFrameworkCore; -namespace Hippocrates.Journal.Data { +namespace EventJournal.Data { public class DatabaseContext : DbContext, IDatabaseContext { - public DbSet Symptoms { get; set; } = null!; - public DbSet Intensities { get; set; } = null!; public DbSet Events { get; set; } = null!; - public DbSet EventTypes { get; set; } = null!; - public DbSet EventSymptoms { get; set; } = null!; + public DbSet EventTypes { get; set; } = null!; + public DbSet Details { get; set; } = null!; + public DbSet DetailTypes { get; set; } = null!; + public DbSet Intensities { get; set; } = null!; public DatabaseContext() { // needed by EF cli tools @@ -30,7 +31,7 @@ public static string GetSqliteDbPath() { // special "local" folder for your platform. var folder = Environment.SpecialFolder.LocalApplicationData; var path = Environment.GetFolderPath(folder); - return Path.Join(path, "PetShop.db"); + return Path.Join(path, "EventJournal.db"); } public Task SaveChangesAsync() { diff --git a/Hippocrates.Journal.Data/DetailRepository.cs b/Hippocrates.Journal.Data/DetailRepository.cs new file mode 100644 index 0000000..a1f402f --- /dev/null +++ b/Hippocrates.Journal.Data/DetailRepository.cs @@ -0,0 +1,6 @@ +using EventJournal.DomainEntities; + +namespace EventJournal.Data { + public class DetailRepository(IDatabaseContext db) : BaseRepository(db, db.Details) { + } +} diff --git a/Hippocrates.Journal.Data/Hippocrates.Journal.Data.csproj b/Hippocrates.Journal.Data/EventJournal.Data.csproj similarity index 92% rename from Hippocrates.Journal.Data/Hippocrates.Journal.Data.csproj rename to Hippocrates.Journal.Data/EventJournal.Data.csproj index 00633f3..1c6a4b3 100644 --- a/Hippocrates.Journal.Data/Hippocrates.Journal.Data.csproj +++ b/Hippocrates.Journal.Data/EventJournal.Data.csproj @@ -15,7 +15,7 @@ - + diff --git a/Hippocrates.Journal.Data/EventRepository.cs b/Hippocrates.Journal.Data/EventRepository.cs index 61d8d07..a062eed 100644 --- a/Hippocrates.Journal.Data/EventRepository.cs +++ b/Hippocrates.Journal.Data/EventRepository.cs @@ -1,12 +1,6 @@ -using Hippocrates.Journal.DomainEntities; +using EventJournal.DomainEntities; -namespace Hippocrates.Journal.Data { +namespace EventJournal.Data { public class EventRepository(IDatabaseContext db) : BaseRepository(db, db.Events) { - protected override void CopyEntity(Event destinationEntity, Event sourceEntity) { - //TODO: should these really call a copy method on the entity itself? - //TODO: could this be done with automapper or some such - destinationEntity.Description = sourceEntity.Description; - destinationEntity.EventTime = sourceEntity.EventTime; - } } } diff --git a/Hippocrates.Journal.Data/EventSymptomRepository.cs b/Hippocrates.Journal.Data/EventSymptomRepository.cs deleted file mode 100644 index 501a264..0000000 --- a/Hippocrates.Journal.Data/EventSymptomRepository.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Hippocrates.Journal.DomainEntities; - -namespace Hippocrates.Journal.Data { - public class EventSymptomRepository(IDatabaseContext db) : BaseRepository(db, db.EventSymptoms) { - protected override void CopyEntity(EventSymptom destinationEntity, EventSymptom sourceEntity) { - //TODO: should these really call a copy method on the entity itself? - //TODO: could this be done with automapper or some such - destinationEntity.Event = sourceEntity.Event; - destinationEntity.Symptom = sourceEntity.Symptom; - destinationEntity.Intensity = sourceEntity.Intensity; - destinationEntity.Notes = sourceEntity.Notes; - } - } -} diff --git a/Hippocrates.Journal.Data/EventTypeRepository.cs b/Hippocrates.Journal.Data/EventTypeRepository.cs deleted file mode 100644 index 672a00c..0000000 --- a/Hippocrates.Journal.Data/EventTypeRepository.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Hippocrates.Journal.DomainEntities; - -namespace Hippocrates.Journal.Data { - public class EventTypeRepository(IDatabaseContext db) : BaseRepository(db, db.EventTypes) { - protected override void CopyEntity(EventType destinationEntity, EventType sourceEntity) { - //TODO: should these really call a copy method on the entity itself? - //TODO: could this be done with automapper or some such - destinationEntity.Description = sourceEntity.Description; - destinationEntity.Name = sourceEntity.Name; - } - } -} diff --git a/Hippocrates.Journal.Data/IDatabaseContext.cs b/Hippocrates.Journal.Data/IDatabaseContext.cs index 631845c..a948307 100644 --- a/Hippocrates.Journal.Data/IDatabaseContext.cs +++ b/Hippocrates.Journal.Data/IDatabaseContext.cs @@ -1,13 +1,14 @@ -using Hippocrates.Journal.DomainEntities; +using EventJournal.DomainEntities; +using EventJournal.DomainEntities.UserTypes; using Microsoft.EntityFrameworkCore; -namespace Hippocrates.Journal.Data { +namespace EventJournal.Data { public interface IDatabaseContext { - DbSet Symptoms { get; set; } - DbSet Intensities { get; set; } DbSet Events { get; set; } DbSet EventTypes { get; set; } - DbSet EventSymptoms { get; set; } + DbSet Details { get; set; } + DbSet DetailTypes { get; set; } + DbSet Intensities { get; set; } Task SaveChangesAsync(); } diff --git a/Hippocrates.Journal.Data/IntensityRepository.cs b/Hippocrates.Journal.Data/IntensityRepository.cs deleted file mode 100644 index 879771c..0000000 --- a/Hippocrates.Journal.Data/IntensityRepository.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Hippocrates.Journal.DomainEntities; - -namespace Hippocrates.Journal.Data { - public class IntensityRepository(IDatabaseContext db) : BaseRepository(db, db.Intensities) { - protected override void CopyEntity(Intensity destinationEntity, Intensity sourceEntity) { - //TODO: could this be done with automapper or some such - destinationEntity.Level = sourceEntity.Level; - destinationEntity.Name = sourceEntity.Name; - destinationEntity.Description = sourceEntity.Description; - destinationEntity.DefaultSortType = sourceEntity.DefaultSortType; - } - } -} diff --git a/Hippocrates.Journal.Data/SymptomRepository.cs b/Hippocrates.Journal.Data/SymptomRepository.cs deleted file mode 100644 index ed49645..0000000 --- a/Hippocrates.Journal.Data/SymptomRepository.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Hippocrates.Journal.DomainEntities; - -namespace Hippocrates.Journal.Data { - public class SymptomRepository(IDatabaseContext db) : BaseRepository(db, db.Symptoms) { - protected override void CopyEntity(Symptom destinationEntity, Symptom sourceEntity) { - //TODO: should these really call a copy method on the entity itself? - //TODO: could this be done with automapper or some such - destinationEntity.Description = sourceEntity.Description; - destinationEntity.Name = sourceEntity.Name; - } - } -} diff --git a/Hippocrates.Journal.Data/UserTypeRepositories/DetailTypeRepository.cs b/Hippocrates.Journal.Data/UserTypeRepositories/DetailTypeRepository.cs new file mode 100644 index 0000000..c2ff763 --- /dev/null +++ b/Hippocrates.Journal.Data/UserTypeRepositories/DetailTypeRepository.cs @@ -0,0 +1,7 @@ +using EventJournal.DomainEntities; +using EventJournal.DomainEntities.UserTypes; + +namespace EventJournal.Data.UserTypeRepositories { + public class DetailTypeRepository(IDatabaseContext db) : BaseRepository(db, db.DetailTypes) { + } +} diff --git a/Hippocrates.Journal.Data/UserTypeRepositories/EventTypeRepository.cs b/Hippocrates.Journal.Data/UserTypeRepositories/EventTypeRepository.cs new file mode 100644 index 0000000..eb2d6b8 --- /dev/null +++ b/Hippocrates.Journal.Data/UserTypeRepositories/EventTypeRepository.cs @@ -0,0 +1,6 @@ +using EventJournal.DomainEntities.UserTypes; + +namespace EventJournal.Data.UserTypeRepositories { + public class EventTypeRepository(IDatabaseContext db) : BaseRepository(db, db.EventTypes) { + } +} diff --git a/Hippocrates.Journal.Data/UserTypeRepositories/IntensityRepository.cs b/Hippocrates.Journal.Data/UserTypeRepositories/IntensityRepository.cs new file mode 100644 index 0000000..c040e98 --- /dev/null +++ b/Hippocrates.Journal.Data/UserTypeRepositories/IntensityRepository.cs @@ -0,0 +1,8 @@ +using EventJournal.DomainEntities; +using EventJournal.DomainEntities.UserTypes; + +namespace EventJournal.Data.UserTypeRepositories { + public class IntensityRepository(IDatabaseContext db) : BaseRepository(db, db.Intensities) { + + } +} diff --git a/README.md b/README.md index 05fa650..8998677 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -# Hippocrates Journal -Collect basic timestamped symptom data with the intent that the data can be analyzed/mined to see longer term patterns. +# Event Journal +Collect basic timestamped event data with the intent that the data can be analyzed/mined to see longer term patterns. + +Originally this was intended to track symptoms and intensity. But evolved into more of an event tracker system. +The concept of Intensity is still a bit muddy, but the idea is that events of a particular even type can be rated on a user defined scale. For example a headache event can have a pain intensity rating. Where an exercise type event could have a distance or workout intensity. -To start, this will focus on symptoms and intensity and intentionally be a single user app. Longer term, the idea is to be able to couple this data with other similar journal data (such as food and medicine tracking). With this in mind the journal entries will be fairly generic in nature. -Maybe rather than tracking symptoms, food, exercise separately, we track EVENTS -with user defined event types such as ## Features @@ -13,8 +13,7 @@ Possible features include: User definable symptom lists User definable symptom intensity descriptions (tied to numeric values for graphing/data presentation purposes) -#### Pain level could be something like - +#### Pain level could be something like - None (0) - Slight to mild Discomfort (1) - I need an OTC painkiller (2) @@ -26,10 +25,10 @@ User definable symptom intensity descriptions (tied to numeric values for graphi - Trace (on TP only) (1) - Minor (drops visible in water) (2) - Major (water mostly red) (3) -- Intense (water completly red) (4) +- Intense (water completely red) (4) journal entries should also have user defined tags -not sure how to implement tags jsut yet, maybe a tag entity and table +not sure how to implement tags just yet, maybe a tag entity and table and a tags list table (with an id) and have journal entry track taglist id From a6cbc8e201f349f39c5e57aa0592a8bbaf5fe857 Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Sun, 22 Jun 2025 17:05:02 -0400 Subject: [PATCH 08/34] reslove notImplemented exceptions --- Hippocrates.Journal-DomainEntities/Detail.cs | 6 +++++- Hippocrates.Journal-DomainEntities/Event.cs | 2 +- .../UserTypes/DetailType.cs | 4 +++- Hippocrates.Journal-DomainEntities/UserTypes/EventType.cs | 8 +++++--- Hippocrates.Journal-DomainEntities/UserTypes/Intensity.cs | 7 ++++++- 5 files changed, 20 insertions(+), 7 deletions(-) diff --git a/Hippocrates.Journal-DomainEntities/Detail.cs b/Hippocrates.Journal-DomainEntities/Detail.cs index 656160f..bb8f4ba 100644 --- a/Hippocrates.Journal-DomainEntities/Detail.cs +++ b/Hippocrates.Journal-DomainEntities/Detail.cs @@ -11,7 +11,11 @@ public class Detail : BaseEntity{ [MaxLength(512)] public string? Notes { get; set; } internal override void CopyUserValues(T source) { - throw new NotImplementedException(); + var sourceDetail = source as Detail ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(Detail)}"); + Event = sourceDetail.Event; + DetailType = sourceDetail.DetailType; + Intensity = sourceDetail.Intensity; + Notes = sourceDetail.Notes; } } } diff --git a/Hippocrates.Journal-DomainEntities/Event.cs b/Hippocrates.Journal-DomainEntities/Event.cs index ab23d9d..7d4de32 100644 --- a/Hippocrates.Journal-DomainEntities/Event.cs +++ b/Hippocrates.Journal-DomainEntities/Event.cs @@ -5,7 +5,7 @@ namespace EventJournal.DomainEntities { public class Event : BaseEntity { [Key] public int EventId { get { return Id; } set { Id = value; } } [Required] public Guid EventResourceId { get { return ResourceId; } set { ResourceId = value; } } - [Required] public EventType Type { get; set; } = EventType.GetDefaultEventType(); + [Required] public EventType Type { get; set; } = EventType.CreateDefaultEventType(); [Required] public DateTime StartTime { get; set; } = DateTime.Now; public DateTime? EndTime { get; set; } = null; [MaxLength(500)] public string? Description { get; set; } diff --git a/Hippocrates.Journal-DomainEntities/UserTypes/DetailType.cs b/Hippocrates.Journal-DomainEntities/UserTypes/DetailType.cs index 70b3530..6b8ffac 100644 --- a/Hippocrates.Journal-DomainEntities/UserTypes/DetailType.cs +++ b/Hippocrates.Journal-DomainEntities/UserTypes/DetailType.cs @@ -10,7 +10,9 @@ public class DetailType : BaseEntity { public IEnumerable Intensities { get; set; } = []; internal override void CopyUserValues(T source) { - throw new NotImplementedException(); + var sourceDetailType = source as DetailType ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(DetailType)}"); + Name = sourceDetailType.Name; + Description = sourceDetailType.Description; } } } diff --git a/Hippocrates.Journal-DomainEntities/UserTypes/EventType.cs b/Hippocrates.Journal-DomainEntities/UserTypes/EventType.cs index f374730..7366878 100644 --- a/Hippocrates.Journal-DomainEntities/UserTypes/EventType.cs +++ b/Hippocrates.Journal-DomainEntities/UserTypes/EventType.cs @@ -10,7 +10,7 @@ public class EventType : BaseEntity { //TODO: this code really belongs in a service or maybe repo public static IEnumerable DefaultEventTypes() { return [ - GetDefaultEventType(), + CreateDefaultEventType(), new EventType { EventTypeResourceId = Guid.NewGuid(), Name = "Exercise" }, new EventType { EventTypeResourceId = Guid.NewGuid(), Name = "Bathroom Visit" }, new EventType { EventTypeResourceId = Guid.NewGuid(), Name = "Food Consumption" }, @@ -18,12 +18,14 @@ public static IEnumerable DefaultEventTypes() { ]; } - public static EventType GetDefaultEventType() { + public static EventType CreateDefaultEventType() { return new EventType { EventTypeResourceId = Guid.NewGuid(), Name = "Random Event", Description = "Use for tracking random things like onset of pain, headache, or whatever that isn't directly associated with a specific even type." }; } internal override void CopyUserValues(T source) { - throw new NotImplementedException(); + var sourceEventType = source as EventType ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(EventType)}"); + Name = sourceEventType.Name; + Description = sourceEventType.Description; } } } diff --git a/Hippocrates.Journal-DomainEntities/UserTypes/Intensity.cs b/Hippocrates.Journal-DomainEntities/UserTypes/Intensity.cs index d197d89..ca2a62c 100644 --- a/Hippocrates.Journal-DomainEntities/UserTypes/Intensity.cs +++ b/Hippocrates.Journal-DomainEntities/UserTypes/Intensity.cs @@ -14,7 +14,12 @@ public class Intensity : BaseEntity { [Required] public required int DetailTypeId { get; set; } internal override void CopyUserValues(T source) { - throw new NotImplementedException(); + var sourceIntensity = source as Intensity ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(Intensity)}"); + Name = sourceIntensity.Name; + Level = sourceIntensity.Level; + Description = sourceIntensity.Description; + DefaultSortType = sourceIntensity.DefaultSortType; + DetailTypeId = sourceIntensity.DetailTypeId; } } From 191cb8a45f8aa22e5d946f559f65919a41db6b6e Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Sun, 22 Jun 2025 17:17:34 -0400 Subject: [PATCH 09/34] fix folder and project names --- .../EventJournal.CLI.csproj | 0 .../Program.cs | 0 .../BaseRepository.cs | 0 .../DatabaseContext.cs | 0 .../DetailRepository.cs | 0 .../EventJournal.Data.csproj | 2 +- .../EventRepository.cs | 0 .../IDatabaseContext.cs | 0 .../UserTypeRepositories/DetailTypeRepository.cs | 0 .../UserTypeRepositories/EventTypeRepository.cs | 0 .../UserTypeRepositories/IntensityRepository.cs | 0 .../BaseEntity.cs | 0 .../Detail.cs | 0 .../Enums/SortType.cs | 0 .../Event.cs | 0 .../EventJournal.DomainEntities.csproj | 0 .../UserTypes/DetailType.cs | 0 .../UserTypes/EventType.cs | 0 .../UserTypes/Intensity.cs | 0 .../Controllers/WeatherForecastController.cs | 0 .../Dockerfile | 12 ++++++------ .../EventJournal.WebAPI.csproj | 2 +- .../EventJournal.WebAPI.http | 0 .../Program.cs | 0 .../Properties/launchSettings.json | 0 .../WeatherForecast.cs | 0 .../appsettings.Development.json | 0 .../appsettings.json | 0 EventJournal.sln | 8 ++++---- README.md | 9 +++++++++ 30 files changed, 21 insertions(+), 12 deletions(-) rename {Hippocrates.Journal-Service.CLI => EventJournal.CLI}/EventJournal.CLI.csproj (100%) rename {Hippocrates.Journal-Service.CLI => EventJournal.CLI}/Program.cs (100%) rename {Hippocrates.Journal.Data => EventJournal.Data}/BaseRepository.cs (100%) rename {Hippocrates.Journal.Data => EventJournal.Data}/DatabaseContext.cs (100%) rename {Hippocrates.Journal.Data => EventJournal.Data}/DetailRepository.cs (100%) rename {Hippocrates.Journal.Data => EventJournal.Data}/EventJournal.Data.csproj (84%) rename {Hippocrates.Journal.Data => EventJournal.Data}/EventRepository.cs (100%) rename {Hippocrates.Journal.Data => EventJournal.Data}/IDatabaseContext.cs (100%) rename {Hippocrates.Journal.Data => EventJournal.Data}/UserTypeRepositories/DetailTypeRepository.cs (100%) rename {Hippocrates.Journal.Data => EventJournal.Data}/UserTypeRepositories/EventTypeRepository.cs (100%) rename {Hippocrates.Journal.Data => EventJournal.Data}/UserTypeRepositories/IntensityRepository.cs (100%) rename {Hippocrates.Journal-DomainEntities => EventJournal.DomainEntities}/BaseEntity.cs (100%) rename {Hippocrates.Journal-DomainEntities => EventJournal.DomainEntities}/Detail.cs (100%) rename {Hippocrates.Journal-DomainEntities => EventJournal.DomainEntities}/Enums/SortType.cs (100%) rename {Hippocrates.Journal-DomainEntities => EventJournal.DomainEntities}/Event.cs (100%) rename {Hippocrates.Journal-DomainEntities => EventJournal.DomainEntities}/EventJournal.DomainEntities.csproj (100%) rename {Hippocrates.Journal-DomainEntities => EventJournal.DomainEntities}/UserTypes/DetailType.cs (100%) rename {Hippocrates.Journal-DomainEntities => EventJournal.DomainEntities}/UserTypes/EventType.cs (100%) rename {Hippocrates.Journal-DomainEntities => EventJournal.DomainEntities}/UserTypes/Intensity.cs (100%) rename {Hippocrates.Journal-WebAPI => EventJournal.WebAPI}/Controllers/WeatherForecastController.cs (100%) rename {Hippocrates.Journal-WebAPI => EventJournal.WebAPI}/Dockerfile (63%) rename {Hippocrates.Journal-WebAPI => EventJournal.WebAPI}/EventJournal.WebAPI.csproj (92%) rename {Hippocrates.Journal-WebAPI => EventJournal.WebAPI}/EventJournal.WebAPI.http (100%) rename {Hippocrates.Journal-WebAPI => EventJournal.WebAPI}/Program.cs (100%) rename {Hippocrates.Journal-WebAPI => EventJournal.WebAPI}/Properties/launchSettings.json (100%) rename {Hippocrates.Journal-WebAPI => EventJournal.WebAPI}/WeatherForecast.cs (100%) rename {Hippocrates.Journal-WebAPI => EventJournal.WebAPI}/appsettings.Development.json (100%) rename {Hippocrates.Journal-WebAPI => EventJournal.WebAPI}/appsettings.json (100%) diff --git a/Hippocrates.Journal-Service.CLI/EventJournal.CLI.csproj b/EventJournal.CLI/EventJournal.CLI.csproj similarity index 100% rename from Hippocrates.Journal-Service.CLI/EventJournal.CLI.csproj rename to EventJournal.CLI/EventJournal.CLI.csproj diff --git a/Hippocrates.Journal-Service.CLI/Program.cs b/EventJournal.CLI/Program.cs similarity index 100% rename from Hippocrates.Journal-Service.CLI/Program.cs rename to EventJournal.CLI/Program.cs diff --git a/Hippocrates.Journal.Data/BaseRepository.cs b/EventJournal.Data/BaseRepository.cs similarity index 100% rename from Hippocrates.Journal.Data/BaseRepository.cs rename to EventJournal.Data/BaseRepository.cs diff --git a/Hippocrates.Journal.Data/DatabaseContext.cs b/EventJournal.Data/DatabaseContext.cs similarity index 100% rename from Hippocrates.Journal.Data/DatabaseContext.cs rename to EventJournal.Data/DatabaseContext.cs diff --git a/Hippocrates.Journal.Data/DetailRepository.cs b/EventJournal.Data/DetailRepository.cs similarity index 100% rename from Hippocrates.Journal.Data/DetailRepository.cs rename to EventJournal.Data/DetailRepository.cs diff --git a/Hippocrates.Journal.Data/EventJournal.Data.csproj b/EventJournal.Data/EventJournal.Data.csproj similarity index 84% rename from Hippocrates.Journal.Data/EventJournal.Data.csproj rename to EventJournal.Data/EventJournal.Data.csproj index 1c6a4b3..54ca306 100644 --- a/Hippocrates.Journal.Data/EventJournal.Data.csproj +++ b/EventJournal.Data/EventJournal.Data.csproj @@ -15,7 +15,7 @@ - + diff --git a/Hippocrates.Journal.Data/EventRepository.cs b/EventJournal.Data/EventRepository.cs similarity index 100% rename from Hippocrates.Journal.Data/EventRepository.cs rename to EventJournal.Data/EventRepository.cs diff --git a/Hippocrates.Journal.Data/IDatabaseContext.cs b/EventJournal.Data/IDatabaseContext.cs similarity index 100% rename from Hippocrates.Journal.Data/IDatabaseContext.cs rename to EventJournal.Data/IDatabaseContext.cs diff --git a/Hippocrates.Journal.Data/UserTypeRepositories/DetailTypeRepository.cs b/EventJournal.Data/UserTypeRepositories/DetailTypeRepository.cs similarity index 100% rename from Hippocrates.Journal.Data/UserTypeRepositories/DetailTypeRepository.cs rename to EventJournal.Data/UserTypeRepositories/DetailTypeRepository.cs diff --git a/Hippocrates.Journal.Data/UserTypeRepositories/EventTypeRepository.cs b/EventJournal.Data/UserTypeRepositories/EventTypeRepository.cs similarity index 100% rename from Hippocrates.Journal.Data/UserTypeRepositories/EventTypeRepository.cs rename to EventJournal.Data/UserTypeRepositories/EventTypeRepository.cs diff --git a/Hippocrates.Journal.Data/UserTypeRepositories/IntensityRepository.cs b/EventJournal.Data/UserTypeRepositories/IntensityRepository.cs similarity index 100% rename from Hippocrates.Journal.Data/UserTypeRepositories/IntensityRepository.cs rename to EventJournal.Data/UserTypeRepositories/IntensityRepository.cs diff --git a/Hippocrates.Journal-DomainEntities/BaseEntity.cs b/EventJournal.DomainEntities/BaseEntity.cs similarity index 100% rename from Hippocrates.Journal-DomainEntities/BaseEntity.cs rename to EventJournal.DomainEntities/BaseEntity.cs diff --git a/Hippocrates.Journal-DomainEntities/Detail.cs b/EventJournal.DomainEntities/Detail.cs similarity index 100% rename from Hippocrates.Journal-DomainEntities/Detail.cs rename to EventJournal.DomainEntities/Detail.cs diff --git a/Hippocrates.Journal-DomainEntities/Enums/SortType.cs b/EventJournal.DomainEntities/Enums/SortType.cs similarity index 100% rename from Hippocrates.Journal-DomainEntities/Enums/SortType.cs rename to EventJournal.DomainEntities/Enums/SortType.cs diff --git a/Hippocrates.Journal-DomainEntities/Event.cs b/EventJournal.DomainEntities/Event.cs similarity index 100% rename from Hippocrates.Journal-DomainEntities/Event.cs rename to EventJournal.DomainEntities/Event.cs diff --git a/Hippocrates.Journal-DomainEntities/EventJournal.DomainEntities.csproj b/EventJournal.DomainEntities/EventJournal.DomainEntities.csproj similarity index 100% rename from Hippocrates.Journal-DomainEntities/EventJournal.DomainEntities.csproj rename to EventJournal.DomainEntities/EventJournal.DomainEntities.csproj diff --git a/Hippocrates.Journal-DomainEntities/UserTypes/DetailType.cs b/EventJournal.DomainEntities/UserTypes/DetailType.cs similarity index 100% rename from Hippocrates.Journal-DomainEntities/UserTypes/DetailType.cs rename to EventJournal.DomainEntities/UserTypes/DetailType.cs diff --git a/Hippocrates.Journal-DomainEntities/UserTypes/EventType.cs b/EventJournal.DomainEntities/UserTypes/EventType.cs similarity index 100% rename from Hippocrates.Journal-DomainEntities/UserTypes/EventType.cs rename to EventJournal.DomainEntities/UserTypes/EventType.cs diff --git a/Hippocrates.Journal-DomainEntities/UserTypes/Intensity.cs b/EventJournal.DomainEntities/UserTypes/Intensity.cs similarity index 100% rename from Hippocrates.Journal-DomainEntities/UserTypes/Intensity.cs rename to EventJournal.DomainEntities/UserTypes/Intensity.cs diff --git a/Hippocrates.Journal-WebAPI/Controllers/WeatherForecastController.cs b/EventJournal.WebAPI/Controllers/WeatherForecastController.cs similarity index 100% rename from Hippocrates.Journal-WebAPI/Controllers/WeatherForecastController.cs rename to EventJournal.WebAPI/Controllers/WeatherForecastController.cs diff --git a/Hippocrates.Journal-WebAPI/Dockerfile b/EventJournal.WebAPI/Dockerfile similarity index 63% rename from Hippocrates.Journal-WebAPI/Dockerfile rename to EventJournal.WebAPI/Dockerfile index eefb74e..b2d300c 100644 --- a/Hippocrates.Journal-WebAPI/Dockerfile +++ b/EventJournal.WebAPI/Dockerfile @@ -12,19 +12,19 @@ EXPOSE 8081 FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build ARG BUILD_CONFIGURATION=Release WORKDIR /src -COPY ["Hippocrates.Journa-WebAPI/Hippocrates.Journa-WebAPI.csproj", "Hippocrates.Journa-WebAPI/"] -RUN dotnet restore "./Hippocrates.Journa-WebAPI/Hippocrates.Journa-WebAPI.csproj" +COPY ["EventJournal.WebAPI/EventJournal.-WebAPI.csproj", "EventJournal.WebAPI/"] +RUN dotnet restore "./EventJournal.WebAPI/EventJournal.WebAPI.csproj" COPY . . -WORKDIR "/src/Hippocrates.Journa-WebAPI" -RUN dotnet build "./Hippocrates.Journa-WebAPI.csproj" -c $BUILD_CONFIGURATION -o /app/build +WORKDIR "/src/EventJournal.WebAPI" +RUN dotnet build "./EventJournal.WebAPI.csproj" -c $BUILD_CONFIGURATION -o /app/build # This stage is used to publish the service project to be copied to the final stage FROM build AS publish ARG BUILD_CONFIGURATION=Release -RUN dotnet publish "./Hippocrates.Journa-WebAPI.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false +RUN dotnet publish "./EventJournal.WebAPI.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false # This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration) FROM base AS final WORKDIR /app COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "Hippocrates.Journa-WebAPI.dll"] \ No newline at end of file +ENTRYPOINT ["dotnet", "EventJournal.WebAPI.dll"] \ No newline at end of file diff --git a/Hippocrates.Journal-WebAPI/EventJournal.WebAPI.csproj b/EventJournal.WebAPI/EventJournal.WebAPI.csproj similarity index 92% rename from Hippocrates.Journal-WebAPI/EventJournal.WebAPI.csproj rename to EventJournal.WebAPI/EventJournal.WebAPI.csproj index 7ed8472..28098f6 100644 --- a/Hippocrates.Journal-WebAPI/EventJournal.WebAPI.csproj +++ b/EventJournal.WebAPI/EventJournal.WebAPI.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - Hippocrates.Journa.WebAPI + EventJournal.WebAPI 8c63a320-2c3e-49fe-b702-bc5449d6d6fa Linux diff --git a/Hippocrates.Journal-WebAPI/EventJournal.WebAPI.http b/EventJournal.WebAPI/EventJournal.WebAPI.http similarity index 100% rename from Hippocrates.Journal-WebAPI/EventJournal.WebAPI.http rename to EventJournal.WebAPI/EventJournal.WebAPI.http diff --git a/Hippocrates.Journal-WebAPI/Program.cs b/EventJournal.WebAPI/Program.cs similarity index 100% rename from Hippocrates.Journal-WebAPI/Program.cs rename to EventJournal.WebAPI/Program.cs diff --git a/Hippocrates.Journal-WebAPI/Properties/launchSettings.json b/EventJournal.WebAPI/Properties/launchSettings.json similarity index 100% rename from Hippocrates.Journal-WebAPI/Properties/launchSettings.json rename to EventJournal.WebAPI/Properties/launchSettings.json diff --git a/Hippocrates.Journal-WebAPI/WeatherForecast.cs b/EventJournal.WebAPI/WeatherForecast.cs similarity index 100% rename from Hippocrates.Journal-WebAPI/WeatherForecast.cs rename to EventJournal.WebAPI/WeatherForecast.cs diff --git a/Hippocrates.Journal-WebAPI/appsettings.Development.json b/EventJournal.WebAPI/appsettings.Development.json similarity index 100% rename from Hippocrates.Journal-WebAPI/appsettings.Development.json rename to EventJournal.WebAPI/appsettings.Development.json diff --git a/Hippocrates.Journal-WebAPI/appsettings.json b/EventJournal.WebAPI/appsettings.json similarity index 100% rename from Hippocrates.Journal-WebAPI/appsettings.json rename to EventJournal.WebAPI/appsettings.json diff --git a/EventJournal.sln b/EventJournal.sln index 0ae955e..126d940 100644 --- a/EventJournal.sln +++ b/EventJournal.sln @@ -3,9 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.12.35521.163 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.CLI", "Hippocrates.Journal-Service.CLI\EventJournal.CLI.csproj", "{D0E93737-A883-4020-9C99-E6C1D70D1237}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.CLI", "EventJournal.Service.CLI\EventJournal.CLI.csproj", "{D0E93737-A883-4020-9C99-E6C1D70D1237}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.DomainEntities", "Hippocrates.Journal-DomainEntities\EventJournal.DomainEntities.csproj", "{2F884BE1-F88D-45F7-A3E0-480FCA94541A}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.DomainEntities", "EventJournal.DomainEntities\EventJournal.DomainEntities.csproj", "{2F884BE1-F88D-45F7-A3E0-480FCA94541A}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{994FEE9A-A85B-4AEC-894B-CD7FEDF3FC09}" ProjectSection(SolutionItems) = preProject @@ -13,9 +13,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.WebAPI", "Hippocrates.Journal-WebAPI\EventJournal.WebAPI.csproj", "{AFC2B8DD-6CDE-450F-9181-93B2B67983D7}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.WebAPI", "EventJournal.WebAPI\EventJournal.WebAPI.csproj", "{AFC2B8DD-6CDE-450F-9181-93B2B67983D7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.Data", "Hippocrates.Journal.Data\EventJournal.Data.csproj", "{CD1F9E2E-4F39-4E6C-B018-B3BF858D57A5}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.Data", "EventJournal.Data\EventJournal.Data.csproj", "{CD1F9E2E-4F39-4E6C-B018-B3BF858D57A5}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/README.md b/README.md index 8998677..aca8856 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,10 @@ Possible features include: User definable symptom lists User definable symptom intensity descriptions (tied to numeric values for graphing/data presentation purposes) +## NOTES + +### Intensity + #### Pain level could be something like - None (0) - Slight to mild Discomfort (1) @@ -32,4 +36,9 @@ not sure how to implement tags just yet, maybe a tag entity and table and a tags list table (with an id) and have journal entry track taglist id +### TODO: +EF https://learn.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs=netcore-cli +add Events domain service - manages Events and Detiails +add UserType service - manages all the User configurable types. + From 06a5e07d59a5a5ae558b2254640e79e8926d74e9 Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Sun, 22 Jun 2025 17:23:36 -0400 Subject: [PATCH 10/34] Initial DB create; nuget updates --- EventJournal.Data/EventJournal.Data.csproj | 4 +- .../20250622211818_InitialCreate.Designer.cs | 252 ++++++++++++++++++ .../20250622211818_InitialCreate.cs | 181 +++++++++++++ .../DatabaseContextModelSnapshot.cs | 249 +++++++++++++++++ .../EventJournal.DomainEntities.csproj | 2 +- .../EventJournal.WebAPI.csproj | 4 +- 6 files changed, 687 insertions(+), 5 deletions(-) create mode 100644 EventJournal.Data/Migrations/20250622211818_InitialCreate.Designer.cs create mode 100644 EventJournal.Data/Migrations/20250622211818_InitialCreate.cs create mode 100644 EventJournal.Data/Migrations/DatabaseContextModelSnapshot.cs diff --git a/EventJournal.Data/EventJournal.Data.csproj b/EventJournal.Data/EventJournal.Data.csproj index 54ca306..bf21534 100644 --- a/EventJournal.Data/EventJournal.Data.csproj +++ b/EventJournal.Data/EventJournal.Data.csproj @@ -7,11 +7,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/EventJournal.Data/Migrations/20250622211818_InitialCreate.Designer.cs b/EventJournal.Data/Migrations/20250622211818_InitialCreate.Designer.cs new file mode 100644 index 0000000..9c75a05 --- /dev/null +++ b/EventJournal.Data/Migrations/20250622211818_InitialCreate.Designer.cs @@ -0,0 +1,252 @@ +// +using System; +using EventJournal.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace EventJournal.Data.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20250622211818_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.3"); + + modelBuilder.Entity("EventJournal.DomainEntities.Detail", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("DetailResourceId") + .HasColumnType("TEXT"); + + b.Property("DetailTypeId") + .HasColumnType("INTEGER"); + + b.Property("EventId") + .HasColumnType("INTEGER"); + + b.Property("IntensityId") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("UpdatedDate") + .HasColumnType("TEXT"); + + b.HasKey("DetailId"); + + b.HasIndex("DetailTypeId"); + + b.HasIndex("EventId"); + + b.HasIndex("IntensityId"); + + b.ToTable("Details"); + }); + + modelBuilder.Entity("EventJournal.DomainEntities.Event", b => + { + b.Property("EventId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("EndTime") + .HasColumnType("TEXT"); + + b.Property("EventResourceId") + .HasColumnType("TEXT"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.Property("TypeEventTypeId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDate") + .HasColumnType("TEXT"); + + b.HasKey("EventId"); + + b.HasIndex("TypeEventTypeId"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("EventJournal.DomainEntities.UserTypes.DetailType", b => + { + b.Property("DetailTypeId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DetailTypeResourceId") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDate") + .HasColumnType("TEXT"); + + b.HasKey("DetailTypeId"); + + b.ToTable("DetailTypes"); + }); + + modelBuilder.Entity("EventJournal.DomainEntities.UserTypes.EventType", b => + { + b.Property("EventTypeId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("EventTypeResourceId") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDate") + .HasColumnType("TEXT"); + + b.HasKey("EventTypeId"); + + b.ToTable("EventTypes"); + }); + + modelBuilder.Entity("EventJournal.DomainEntities.UserTypes.Intensity", b => + { + b.Property("IntensityId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("DefaultSortType") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DetailTypeId") + .HasColumnType("INTEGER"); + + b.Property("IntensityResourceId") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("UpdatedDate") + .HasColumnType("TEXT"); + + b.HasKey("IntensityId"); + + b.HasIndex("DetailTypeId"); + + b.ToTable("Intensities"); + }); + + modelBuilder.Entity("EventJournal.DomainEntities.Detail", b => + { + b.HasOne("EventJournal.DomainEntities.UserTypes.DetailType", "DetailType") + .WithMany() + .HasForeignKey("DetailTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("EventJournal.DomainEntities.Event", "Event") + .WithMany("Details") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("EventJournal.DomainEntities.UserTypes.Intensity", "Intensity") + .WithMany() + .HasForeignKey("IntensityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DetailType"); + + b.Navigation("Event"); + + b.Navigation("Intensity"); + }); + + modelBuilder.Entity("EventJournal.DomainEntities.Event", b => + { + b.HasOne("EventJournal.DomainEntities.UserTypes.EventType", "Type") + .WithMany() + .HasForeignKey("TypeEventTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Type"); + }); + + modelBuilder.Entity("EventJournal.DomainEntities.UserTypes.Intensity", b => + { + b.HasOne("EventJournal.DomainEntities.UserTypes.DetailType", null) + .WithMany("Intensities") + .HasForeignKey("DetailTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("EventJournal.DomainEntities.Event", b => + { + b.Navigation("Details"); + }); + + modelBuilder.Entity("EventJournal.DomainEntities.UserTypes.DetailType", b => + { + b.Navigation("Intensities"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/EventJournal.Data/Migrations/20250622211818_InitialCreate.cs b/EventJournal.Data/Migrations/20250622211818_InitialCreate.cs new file mode 100644 index 0000000..175582c --- /dev/null +++ b/EventJournal.Data/Migrations/20250622211818_InitialCreate.cs @@ -0,0 +1,181 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace EventJournal.Data.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "DetailTypes", + columns: table => new + { + DetailTypeId = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + DetailTypeResourceId = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + Description = table.Column(type: "TEXT", maxLength: 500, nullable: true), + CreatedDate = table.Column(type: "TEXT", nullable: false), + UpdatedDate = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_DetailTypes", x => x.DetailTypeId); + }); + + migrationBuilder.CreateTable( + name: "EventTypes", + columns: table => new + { + EventTypeId = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + EventTypeResourceId = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + Description = table.Column(type: "TEXT", maxLength: 500, nullable: true), + CreatedDate = table.Column(type: "TEXT", nullable: false), + UpdatedDate = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_EventTypes", x => x.EventTypeId); + }); + + migrationBuilder.CreateTable( + name: "Intensities", + columns: table => new + { + IntensityId = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + IntensityResourceId = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 50, nullable: false), + Level = table.Column(type: "INTEGER", nullable: false), + Description = table.Column(type: "TEXT", maxLength: 500, nullable: true), + DefaultSortType = table.Column(type: "INTEGER", nullable: false), + DetailTypeId = table.Column(type: "INTEGER", nullable: false), + CreatedDate = table.Column(type: "TEXT", nullable: false), + UpdatedDate = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Intensities", x => x.IntensityId); + table.ForeignKey( + name: "FK_Intensities_DetailTypes_DetailTypeId", + column: x => x.DetailTypeId, + principalTable: "DetailTypes", + principalColumn: "DetailTypeId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Events", + columns: table => new + { + EventId = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + EventResourceId = table.Column(type: "TEXT", nullable: false), + TypeEventTypeId = table.Column(type: "INTEGER", nullable: false), + StartTime = table.Column(type: "TEXT", nullable: false), + EndTime = table.Column(type: "TEXT", nullable: true), + Description = table.Column(type: "TEXT", maxLength: 500, nullable: true), + CreatedDate = table.Column(type: "TEXT", nullable: false), + UpdatedDate = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Events", x => x.EventId); + table.ForeignKey( + name: "FK_Events_EventTypes_TypeEventTypeId", + column: x => x.TypeEventTypeId, + principalTable: "EventTypes", + principalColumn: "EventTypeId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Details", + columns: table => new + { + DetailId = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + DetailResourceId = table.Column(type: "TEXT", nullable: false), + EventId = table.Column(type: "INTEGER", nullable: false), + DetailTypeId = table.Column(type: "INTEGER", nullable: false), + IntensityId = table.Column(type: "INTEGER", nullable: false), + Notes = table.Column(type: "TEXT", maxLength: 512, nullable: true), + CreatedDate = table.Column(type: "TEXT", nullable: false), + UpdatedDate = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Details", x => x.DetailId); + table.ForeignKey( + name: "FK_Details_DetailTypes_DetailTypeId", + column: x => x.DetailTypeId, + principalTable: "DetailTypes", + principalColumn: "DetailTypeId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Details_Events_EventId", + column: x => x.EventId, + principalTable: "Events", + principalColumn: "EventId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Details_Intensities_IntensityId", + column: x => x.IntensityId, + principalTable: "Intensities", + principalColumn: "IntensityId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Details_DetailTypeId", + table: "Details", + column: "DetailTypeId"); + + migrationBuilder.CreateIndex( + name: "IX_Details_EventId", + table: "Details", + column: "EventId"); + + migrationBuilder.CreateIndex( + name: "IX_Details_IntensityId", + table: "Details", + column: "IntensityId"); + + migrationBuilder.CreateIndex( + name: "IX_Events_TypeEventTypeId", + table: "Events", + column: "TypeEventTypeId"); + + migrationBuilder.CreateIndex( + name: "IX_Intensities_DetailTypeId", + table: "Intensities", + column: "DetailTypeId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Details"); + + migrationBuilder.DropTable( + name: "Events"); + + migrationBuilder.DropTable( + name: "Intensities"); + + migrationBuilder.DropTable( + name: "EventTypes"); + + migrationBuilder.DropTable( + name: "DetailTypes"); + } + } +} diff --git a/EventJournal.Data/Migrations/DatabaseContextModelSnapshot.cs b/EventJournal.Data/Migrations/DatabaseContextModelSnapshot.cs new file mode 100644 index 0000000..34999c8 --- /dev/null +++ b/EventJournal.Data/Migrations/DatabaseContextModelSnapshot.cs @@ -0,0 +1,249 @@ +// +using System; +using EventJournal.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace EventJournal.Data.Migrations +{ + [DbContext(typeof(DatabaseContext))] + partial class DatabaseContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.3"); + + modelBuilder.Entity("EventJournal.DomainEntities.Detail", b => + { + b.Property("DetailId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("DetailResourceId") + .HasColumnType("TEXT"); + + b.Property("DetailTypeId") + .HasColumnType("INTEGER"); + + b.Property("EventId") + .HasColumnType("INTEGER"); + + b.Property("IntensityId") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("UpdatedDate") + .HasColumnType("TEXT"); + + b.HasKey("DetailId"); + + b.HasIndex("DetailTypeId"); + + b.HasIndex("EventId"); + + b.HasIndex("IntensityId"); + + b.ToTable("Details"); + }); + + modelBuilder.Entity("EventJournal.DomainEntities.Event", b => + { + b.Property("EventId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("EndTime") + .HasColumnType("TEXT"); + + b.Property("EventResourceId") + .HasColumnType("TEXT"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.Property("TypeEventTypeId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDate") + .HasColumnType("TEXT"); + + b.HasKey("EventId"); + + b.HasIndex("TypeEventTypeId"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("EventJournal.DomainEntities.UserTypes.DetailType", b => + { + b.Property("DetailTypeId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DetailTypeResourceId") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDate") + .HasColumnType("TEXT"); + + b.HasKey("DetailTypeId"); + + b.ToTable("DetailTypes"); + }); + + modelBuilder.Entity("EventJournal.DomainEntities.UserTypes.EventType", b => + { + b.Property("EventTypeId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("EventTypeResourceId") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDate") + .HasColumnType("TEXT"); + + b.HasKey("EventTypeId"); + + b.ToTable("EventTypes"); + }); + + modelBuilder.Entity("EventJournal.DomainEntities.UserTypes.Intensity", b => + { + b.Property("IntensityId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("DefaultSortType") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DetailTypeId") + .HasColumnType("INTEGER"); + + b.Property("IntensityResourceId") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("UpdatedDate") + .HasColumnType("TEXT"); + + b.HasKey("IntensityId"); + + b.HasIndex("DetailTypeId"); + + b.ToTable("Intensities"); + }); + + modelBuilder.Entity("EventJournal.DomainEntities.Detail", b => + { + b.HasOne("EventJournal.DomainEntities.UserTypes.DetailType", "DetailType") + .WithMany() + .HasForeignKey("DetailTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("EventJournal.DomainEntities.Event", "Event") + .WithMany("Details") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("EventJournal.DomainEntities.UserTypes.Intensity", "Intensity") + .WithMany() + .HasForeignKey("IntensityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DetailType"); + + b.Navigation("Event"); + + b.Navigation("Intensity"); + }); + + modelBuilder.Entity("EventJournal.DomainEntities.Event", b => + { + b.HasOne("EventJournal.DomainEntities.UserTypes.EventType", "Type") + .WithMany() + .HasForeignKey("TypeEventTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Type"); + }); + + modelBuilder.Entity("EventJournal.DomainEntities.UserTypes.Intensity", b => + { + b.HasOne("EventJournal.DomainEntities.UserTypes.DetailType", null) + .WithMany("Intensities") + .HasForeignKey("DetailTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("EventJournal.DomainEntities.Event", b => + { + b.Navigation("Details"); + }); + + modelBuilder.Entity("EventJournal.DomainEntities.UserTypes.DetailType", b => + { + b.Navigation("Intensities"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/EventJournal.DomainEntities/EventJournal.DomainEntities.csproj b/EventJournal.DomainEntities/EventJournal.DomainEntities.csproj index 4e0e8d4..58019fc 100644 --- a/EventJournal.DomainEntities/EventJournal.DomainEntities.csproj +++ b/EventJournal.DomainEntities/EventJournal.DomainEntities.csproj @@ -8,7 +8,7 @@ - + diff --git a/EventJournal.WebAPI/EventJournal.WebAPI.csproj b/EventJournal.WebAPI/EventJournal.WebAPI.csproj index 28098f6..ffca7f5 100644 --- a/EventJournal.WebAPI/EventJournal.WebAPI.csproj +++ b/EventJournal.WebAPI/EventJournal.WebAPI.csproj @@ -10,12 +10,12 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + From 32ff43ab8098395755b0fd725d2d8a2dfa146c50 Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Sun, 17 Aug 2025 15:16:23 -0400 Subject: [PATCH 11/34] Update TODO, add stubbed microservices`; git push ` --- EventJournal.CLI/EventJournal.CLI.csproj | 3 +- EventJournal.DomainEntities/BaseEntity.cs | 4 +- .../EventJournal.DomainService.csproj | 14 ++++++ EventJournal.DomainService/EventService.cs | 5 ++ EventJournal.DomainService/UserTypeService.cs | 4 ++ EventJournal.sln | 8 +++- README.md | 46 ++++++++++--------- 7 files changed, 59 insertions(+), 25 deletions(-) create mode 100644 EventJournal.DomainService/EventJournal.DomainService.csproj create mode 100644 EventJournal.DomainService/EventService.cs create mode 100644 EventJournal.DomainService/UserTypeService.cs diff --git a/EventJournal.CLI/EventJournal.CLI.csproj b/EventJournal.CLI/EventJournal.CLI.csproj index 69ce840..748adb0 100644 --- a/EventJournal.CLI/EventJournal.CLI.csproj +++ b/EventJournal.CLI/EventJournal.CLI.csproj @@ -18,6 +18,7 @@ - + + diff --git a/EventJournal.DomainEntities/BaseEntity.cs b/EventJournal.DomainEntities/BaseEntity.cs index c706c20..029a472 100644 --- a/EventJournal.DomainEntities/BaseEntity.cs +++ b/EventJournal.DomainEntities/BaseEntity.cs @@ -3,8 +3,8 @@ namespace EventJournal.DomainEntities { public abstract class BaseEntity { - [Required] public DateTime CreatedDate { get; set; } = DateTime.Now; - [Required] public DateTime UpdatedDate { get; set; } = DateTime.MinValue; + [Required] public DateTime CreatedDate { get; set; } = DateTime.UtcNow; + [Required] public DateTime UpdatedDate { get; set; } = DateTime.UtcNow; [NotMapped] [Required] public int Id { get; set; } [NotMapped] diff --git a/EventJournal.DomainService/EventJournal.DomainService.csproj b/EventJournal.DomainService/EventJournal.DomainService.csproj new file mode 100644 index 0000000..03d4945 --- /dev/null +++ b/EventJournal.DomainService/EventJournal.DomainService.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/EventJournal.DomainService/EventService.cs b/EventJournal.DomainService/EventService.cs new file mode 100644 index 0000000..0f604d5 --- /dev/null +++ b/EventJournal.DomainService/EventService.cs @@ -0,0 +1,5 @@ +namespace EventJournal.DomainService { + public class EventService { + + } +} diff --git a/EventJournal.DomainService/UserTypeService.cs b/EventJournal.DomainService/UserTypeService.cs new file mode 100644 index 0000000..166803e --- /dev/null +++ b/EventJournal.DomainService/UserTypeService.cs @@ -0,0 +1,4 @@ +namespace EventJournal.DomainService { + public class UserTypeService { + } +} diff --git a/EventJournal.sln b/EventJournal.sln index 126d940..a7e4fab 100644 --- a/EventJournal.sln +++ b/EventJournal.sln @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.12.35521.163 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.CLI", "EventJournal.Service.CLI\EventJournal.CLI.csproj", "{D0E93737-A883-4020-9C99-E6C1D70D1237}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.CLI", "EventJournal.CLI\EventJournal.CLI.csproj", "{D0E93737-A883-4020-9C99-E6C1D70D1237}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.DomainEntities", "EventJournal.DomainEntities\EventJournal.DomainEntities.csproj", "{2F884BE1-F88D-45F7-A3E0-480FCA94541A}" EndProject @@ -17,6 +17,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.WebAPI", "Even EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.Data", "EventJournal.Data\EventJournal.Data.csproj", "{CD1F9E2E-4F39-4E6C-B018-B3BF858D57A5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.DomainService", "EventJournal.DomainService\EventJournal.DomainService.csproj", "{EB6275FF-4E73-466D-9A44-EFDBF992CA75}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -39,6 +41,10 @@ Global {CD1F9E2E-4F39-4E6C-B018-B3BF858D57A5}.Debug|Any CPU.Build.0 = Debug|Any CPU {CD1F9E2E-4F39-4E6C-B018-B3BF858D57A5}.Release|Any CPU.ActiveCfg = Release|Any CPU {CD1F9E2E-4F39-4E6C-B018-B3BF858D57A5}.Release|Any CPU.Build.0 = Release|Any CPU + {EB6275FF-4E73-466D-9A44-EFDBF992CA75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EB6275FF-4E73-466D-9A44-EFDBF992CA75}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB6275FF-4E73-466D-9A44-EFDBF992CA75}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EB6275FF-4E73-466D-9A44-EFDBF992CA75}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/README.md b/README.md index aca8856..1de81ae 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,48 @@ # Event Journal -Collect basic timestamped event data with the intent that the data can be analyzed/mined to see longer term patterns. -Originally this was intended to track symptoms and intensity. But evolved into more of an event tracker system. +Collect basic timestamped event data with the intent that the data can be analyzed/mined to see longer term patterns. + +Originally this was intended to track symptoms and intensity. But evolved into more of an event tracker system. The concept of Intensity is still a bit muddy, but the idea is that events of a particular even type can be rated on a user defined scale. For example a headache event can have a pain intensity rating. Where an exercise type event could have a distance or workout intensity. +## Features +Features include: +User definable event types (ex. exercise, meal, headache ) +USer definable detail types (ex. muscle pain, bleeding, restaurant or food eaten, pain location, pain level, nausea ) +User definable detail intensity descriptions (tied to numeric values for graphing/data presentation purposes) -## Features +## NOTES +### Entities -Possible features include: -User definable symptom lists -User definable symptom intensity descriptions (tied to numeric values for graphing/data presentation purposes) +Events are the central entity. An `Event` has an `EventType` and one or more `Detail` records. Each `Detail` record has a `DetailType` and an `Intensity`. +`EventType`, `DetailType`, and `Intensity` are user defined types. -## NOTES +### Intensity Examples -### Intensity +#### Pain level could be something like -#### Pain level could be something like -- None (0) +- None (0) - Slight to mild Discomfort (1) - I need an OTC painkiller (2) - I'm going to limit my activity (3) - I can't function (4) - Take me to the ER (5) -#### Bleeding could be + +#### Bleeding could be + - None (0) -- Trace (on TP only) (1) +- Trace (on TP only) (1) - Minor (drops visible in water) (2) - Major (water mostly red) (3) - Intense (water completely red) (4) -journal entries should also have user defined tags -not sure how to implement tags just yet, maybe a tag entity and table -and a tags list table (with an id) and have journal entry track taglist id - - -### TODO: -EF https://learn.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs=netcore-cli -add Events domain service - manages Events and Detiails -add UserType service - manages all the User configurable types. +### TODO +populate Events domain service - manages Events and Details +populate UserType service - manages all the User configurable types. +Event entries should also have user defined tags. +not sure how to implement tags just yet, maybe a tag entity and table +and a tags list table (with an id) and have journal entry track tag-list id From c49299f8b89bfed32baea00a101fe5ec1f321d49 Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Sun, 17 Aug 2025 15:21:09 -0400 Subject: [PATCH 12/34] update nuget pkgs --- EventJournal.CLI/EventJournal.CLI.csproj | 6 +++--- EventJournal.Data/EventJournal.Data.csproj | 4 ++-- .../EventJournal.DomainEntities.csproj | 2 +- EventJournal.WebAPI/EventJournal.WebAPI.csproj | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/EventJournal.CLI/EventJournal.CLI.csproj b/EventJournal.CLI/EventJournal.CLI.csproj index 748adb0..8002167 100644 --- a/EventJournal.CLI/EventJournal.CLI.csproj +++ b/EventJournal.CLI/EventJournal.CLI.csproj @@ -9,12 +9,12 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/EventJournal.Data/EventJournal.Data.csproj b/EventJournal.Data/EventJournal.Data.csproj index bf21534..638e055 100644 --- a/EventJournal.Data/EventJournal.Data.csproj +++ b/EventJournal.Data/EventJournal.Data.csproj @@ -7,11 +7,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/EventJournal.DomainEntities/EventJournal.DomainEntities.csproj b/EventJournal.DomainEntities/EventJournal.DomainEntities.csproj index 58019fc..4663be5 100644 --- a/EventJournal.DomainEntities/EventJournal.DomainEntities.csproj +++ b/EventJournal.DomainEntities/EventJournal.DomainEntities.csproj @@ -8,7 +8,7 @@ - + diff --git a/EventJournal.WebAPI/EventJournal.WebAPI.csproj b/EventJournal.WebAPI/EventJournal.WebAPI.csproj index ffca7f5..744fc6a 100644 --- a/EventJournal.WebAPI/EventJournal.WebAPI.csproj +++ b/EventJournal.WebAPI/EventJournal.WebAPI.csproj @@ -10,12 +10,12 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + From 3d9a9a14898a27f905c27b3e831eca8c85bbf298 Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Sun, 17 Aug 2025 17:41:33 -0400 Subject: [PATCH 13/34] add repostiory interfaces, clean up repository methods, add event service and user type service, update projects to C#9 --- EventJournal.CLI/EventJournal.CLI.csproj | 3 +- EventJournal.CLI/Program.cs | 14 ++--- EventJournal.Data/BaseRepository.cs | 32 +++++++----- EventJournal.Data/DetailRepository.cs | 2 +- EventJournal.Data/EventJournal.Data.csproj | 2 +- EventJournal.Data/EventRepository.cs | 2 +- EventJournal.Data/IBaseRepository.cs | 10 ++++ EventJournal.Data/IDetailRepository.cs | 6 +++ EventJournal.Data/IEventRepository.cs | 6 +++ .../DetailTypeRepository.cs | 2 +- .../EventTypeRepository.cs | 2 +- .../IDetailTypeRepository.cs | 6 +++ .../IEventTypeRepository.cs | 6 +++ .../IIntensityRepository.cs | 6 +++ .../IntensityRepository.cs | 2 +- .../{Enums => Enumerations}/SortType.cs | 2 +- .../EventJournal.DomainEntities.csproj | 2 +- .../UserTypes/Intensity.cs | 2 +- EventJournal.DomainModels/Class1.cs | 5 ++ .../EventJournal.DomainModels.csproj | 9 ++++ .../EventJournal.DomainService.csproj | 2 +- EventJournal.DomainService/EventService.cs | 36 ++++++++++++- EventJournal.DomainService/IEventService.cs | 15 ++++++ .../IUserTypeService.cs | 4 ++ EventJournal.DomainService/UserTypeService.cs | 51 ++++++++++++++++++- .../EventJournal.WebAPI.csproj | 4 +- EventJournal.sln | 6 +++ README.md | 27 ++++++++-- 28 files changed, 226 insertions(+), 40 deletions(-) create mode 100644 EventJournal.Data/IBaseRepository.cs create mode 100644 EventJournal.Data/IDetailRepository.cs create mode 100644 EventJournal.Data/IEventRepository.cs create mode 100644 EventJournal.Data/UserTypeRepositories/IDetailTypeRepository.cs create mode 100644 EventJournal.Data/UserTypeRepositories/IEventTypeRepository.cs create mode 100644 EventJournal.Data/UserTypeRepositories/IIntensityRepository.cs rename EventJournal.DomainEntities/{Enums => Enumerations}/SortType.cs (56%) create mode 100644 EventJournal.DomainModels/Class1.cs create mode 100644 EventJournal.DomainModels/EventJournal.DomainModels.csproj create mode 100644 EventJournal.DomainService/IEventService.cs create mode 100644 EventJournal.DomainService/IUserTypeService.cs diff --git a/EventJournal.CLI/EventJournal.CLI.csproj b/EventJournal.CLI/EventJournal.CLI.csproj index 8002167..6fd7bd3 100644 --- a/EventJournal.CLI/EventJournal.CLI.csproj +++ b/EventJournal.CLI/EventJournal.CLI.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net9.0 EventJournal.CLI enable enable @@ -18,6 +18,7 @@ + diff --git a/EventJournal.CLI/Program.cs b/EventJournal.CLI/Program.cs index fd078b8..d3ae0f7 100644 --- a/EventJournal.CLI/Program.cs +++ b/EventJournal.CLI/Program.cs @@ -1,5 +1,6 @@ // See https://aka.ms/new-console-template for more information using EventJournal.Data; +using EventJournal.DomainService; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -8,18 +9,19 @@ internal class Program { private static void Main(string[] args) { var services = CreateServiceCollection(); - //var ProductService = services.GetService() ?? throw new Exception("Unable to locate a valid Product Logic module"); - //var OrderService = services.GetService() ?? throw new Exception("Unable to locate a valid Order Logic module"); + var EventService = services.GetService() ?? throw new Exception("Unable to locate a valid Product Logic module"); + var UserTypeServes = services.GetService() ?? throw new Exception("Unable to locate a valid Order Logic module"); + //TODO: move to shared startup.cs static IServiceProvider CreateServiceCollection() { var servicecollection = new ServiceCollection() .AddDbContext(options => { options.UseSqlite($"Data Source={DatabaseContext.GetSqliteDbPath()}"); }) - //.AddSingleton() - //.AddSingleton() - //.AddSingleton() - //.AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() .AddLogging(options => { options.AddDebug(); options.SetMinimumLevel(LogLevel.Error); diff --git a/EventJournal.Data/BaseRepository.cs b/EventJournal.Data/BaseRepository.cs index e087f4d..51736ba 100644 --- a/EventJournal.Data/BaseRepository.cs +++ b/EventJournal.Data/BaseRepository.cs @@ -2,39 +2,45 @@ using Microsoft.EntityFrameworkCore; namespace EventJournal.Data { - public abstract class BaseRepository(IDatabaseContext db, DbSet table) where T : BaseEntity{ + public abstract class BaseRepository(IDatabaseContext db, DbSet table) : IBaseRepository where T : BaseEntity { private readonly IDatabaseContext db = db; private readonly DbSet table = table; - public async Task AddAsync(T entity) { + internal async Task AddAsync(T entity) { var row = await table.AddAsync(entity).ConfigureAwait(false); await db.SaveChangesAsync().ConfigureAwait(false); return row.Entity; } - public async Task RemoveAsync(T entity) { - table.Remove(entity); - await db.SaveChangesAsync().ConfigureAwait(false); + + public async Task> GetAllAsync() { + return await table.ToListAsync().ConfigureAwait(false); } - public async Task GetAsync(int id) { + + internal async Task GetByIdAsync(int id) { return await table.FindAsync(id).ConfigureAwait(false); } - public async Task GetAsync(Guid resourceId) { - return await table.FirstOrDefaultAsync(t => t.ResourceId == resourceId).ConfigureAwait(false); + public async Task GetByResourceIdAsync(Guid resourceId) { + return await table.FirstOrDefaultAsync(t => t.ResourceId == resourceId).ConfigureAwait(false); + } + + public async Task DeleteAsync(T entity) { + table.Remove(entity); + await db.SaveChangesAsync().ConfigureAwait(false); } // TODO: this seems like it could/should be an extension method - public async Task SaveEntity(T source) { + public async Task AddUpdateAsync(T source) { ArgumentException.ThrowIfNullOrEmpty(nameof(source)); - var existingEntity = await GetAsync(source.Id) - ?? await GetAsync(source.ResourceId); + var existingEntity = await GetByIdAsync(source.Id) + ?? await GetByResourceIdAsync(source.ResourceId).ConfigureAwait(false); if (existingEntity == null) { return await AddAsync(source).ConfigureAwait(false); } - + existingEntity.UpdateEntity(source); await db.SaveChangesAsync().ConfigureAwait(false); - return existingEntity; + return existingEntity; } } } diff --git a/EventJournal.Data/DetailRepository.cs b/EventJournal.Data/DetailRepository.cs index a1f402f..5f27a00 100644 --- a/EventJournal.Data/DetailRepository.cs +++ b/EventJournal.Data/DetailRepository.cs @@ -1,6 +1,6 @@ using EventJournal.DomainEntities; namespace EventJournal.Data { - public class DetailRepository(IDatabaseContext db) : BaseRepository(db, db.Details) { + public class DetailRepository(IDatabaseContext db) : BaseRepository(db, db.Details), IDetailRepository { } } diff --git a/EventJournal.Data/EventJournal.Data.csproj b/EventJournal.Data/EventJournal.Data.csproj index 638e055..1ba8487 100644 --- a/EventJournal.Data/EventJournal.Data.csproj +++ b/EventJournal.Data/EventJournal.Data.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 enable enable diff --git a/EventJournal.Data/EventRepository.cs b/EventJournal.Data/EventRepository.cs index a062eed..1a46457 100644 --- a/EventJournal.Data/EventRepository.cs +++ b/EventJournal.Data/EventRepository.cs @@ -1,6 +1,6 @@ using EventJournal.DomainEntities; namespace EventJournal.Data { - public class EventRepository(IDatabaseContext db) : BaseRepository(db, db.Events) { + public class EventRepository(IDatabaseContext db) : BaseRepository(db, db.Events), IEventRepository { } } diff --git a/EventJournal.Data/IBaseRepository.cs b/EventJournal.Data/IBaseRepository.cs new file mode 100644 index 0000000..f7db9a2 --- /dev/null +++ b/EventJournal.Data/IBaseRepository.cs @@ -0,0 +1,10 @@ +using EventJournal.DomainEntities; + +namespace EventJournal.Data { + public interface IBaseRepository where T : BaseEntity { + Task> GetAllAsync(); + Task GetByResourceIdAsync(Guid resourceId); + Task AddUpdateAsync(T source); + Task DeleteAsync(T entity); + } +} \ No newline at end of file diff --git a/EventJournal.Data/IDetailRepository.cs b/EventJournal.Data/IDetailRepository.cs new file mode 100644 index 0000000..1949970 --- /dev/null +++ b/EventJournal.Data/IDetailRepository.cs @@ -0,0 +1,6 @@ +using EventJournal.DomainEntities; + +namespace EventJournal.Data { + public interface IDetailRepository : IBaseRepository { + } +} \ No newline at end of file diff --git a/EventJournal.Data/IEventRepository.cs b/EventJournal.Data/IEventRepository.cs new file mode 100644 index 0000000..6cedaeb --- /dev/null +++ b/EventJournal.Data/IEventRepository.cs @@ -0,0 +1,6 @@ +using EventJournal.DomainEntities; + +namespace EventJournal.Data { + public interface IEventRepository : IBaseRepository { + } +} \ No newline at end of file diff --git a/EventJournal.Data/UserTypeRepositories/DetailTypeRepository.cs b/EventJournal.Data/UserTypeRepositories/DetailTypeRepository.cs index c2ff763..31ae222 100644 --- a/EventJournal.Data/UserTypeRepositories/DetailTypeRepository.cs +++ b/EventJournal.Data/UserTypeRepositories/DetailTypeRepository.cs @@ -2,6 +2,6 @@ using EventJournal.DomainEntities.UserTypes; namespace EventJournal.Data.UserTypeRepositories { - public class DetailTypeRepository(IDatabaseContext db) : BaseRepository(db, db.DetailTypes) { + public class DetailTypeRepository(IDatabaseContext db) : BaseRepository(db, db.DetailTypes), IDetailTypeRepository { } } diff --git a/EventJournal.Data/UserTypeRepositories/EventTypeRepository.cs b/EventJournal.Data/UserTypeRepositories/EventTypeRepository.cs index eb2d6b8..c603a4f 100644 --- a/EventJournal.Data/UserTypeRepositories/EventTypeRepository.cs +++ b/EventJournal.Data/UserTypeRepositories/EventTypeRepository.cs @@ -1,6 +1,6 @@ using EventJournal.DomainEntities.UserTypes; namespace EventJournal.Data.UserTypeRepositories { - public class EventTypeRepository(IDatabaseContext db) : BaseRepository(db, db.EventTypes) { + public class EventTypeRepository(IDatabaseContext db) : BaseRepository(db, db.EventTypes), IEventTypeRepository { } } diff --git a/EventJournal.Data/UserTypeRepositories/IDetailTypeRepository.cs b/EventJournal.Data/UserTypeRepositories/IDetailTypeRepository.cs new file mode 100644 index 0000000..6454766 --- /dev/null +++ b/EventJournal.Data/UserTypeRepositories/IDetailTypeRepository.cs @@ -0,0 +1,6 @@ +using EventJournal.DomainEntities.UserTypes; + +namespace EventJournal.Data.UserTypeRepositories { + public interface IDetailTypeRepository : IBaseRepository { + } +} \ No newline at end of file diff --git a/EventJournal.Data/UserTypeRepositories/IEventTypeRepository.cs b/EventJournal.Data/UserTypeRepositories/IEventTypeRepository.cs new file mode 100644 index 0000000..b399343 --- /dev/null +++ b/EventJournal.Data/UserTypeRepositories/IEventTypeRepository.cs @@ -0,0 +1,6 @@ +using EventJournal.DomainEntities.UserTypes; + +namespace EventJournal.Data.UserTypeRepositories { + public interface IEventTypeRepository : IBaseRepository { + } +} \ No newline at end of file diff --git a/EventJournal.Data/UserTypeRepositories/IIntensityRepository.cs b/EventJournal.Data/UserTypeRepositories/IIntensityRepository.cs new file mode 100644 index 0000000..fbed842 --- /dev/null +++ b/EventJournal.Data/UserTypeRepositories/IIntensityRepository.cs @@ -0,0 +1,6 @@ +using EventJournal.DomainEntities.UserTypes; + +namespace EventJournal.Data.UserTypeRepositories { + public interface IIntensityRepository : IBaseRepository { + } +} \ No newline at end of file diff --git a/EventJournal.Data/UserTypeRepositories/IntensityRepository.cs b/EventJournal.Data/UserTypeRepositories/IntensityRepository.cs index c040e98..3b7b53d 100644 --- a/EventJournal.Data/UserTypeRepositories/IntensityRepository.cs +++ b/EventJournal.Data/UserTypeRepositories/IntensityRepository.cs @@ -2,7 +2,7 @@ using EventJournal.DomainEntities.UserTypes; namespace EventJournal.Data.UserTypeRepositories { - public class IntensityRepository(IDatabaseContext db) : BaseRepository(db, db.Intensities) { + public class IntensityRepository(IDatabaseContext db) : BaseRepository(db, db.Intensities), IIntensityRepository { } } diff --git a/EventJournal.DomainEntities/Enums/SortType.cs b/EventJournal.DomainEntities/Enumerations/SortType.cs similarity index 56% rename from EventJournal.DomainEntities/Enums/SortType.cs rename to EventJournal.DomainEntities/Enumerations/SortType.cs index c7bd6cd..94c2f20 100644 --- a/EventJournal.DomainEntities/Enums/SortType.cs +++ b/EventJournal.DomainEntities/Enumerations/SortType.cs @@ -1,4 +1,4 @@ -namespace EventJournal.DomainEntities.Enums { +namespace EventJournal.DomainEntities.Enumerations { public enum SortType { Custom, Ascending, Descending } diff --git a/EventJournal.DomainEntities/EventJournal.DomainEntities.csproj b/EventJournal.DomainEntities/EventJournal.DomainEntities.csproj index 4663be5..1f5a270 100644 --- a/EventJournal.DomainEntities/EventJournal.DomainEntities.csproj +++ b/EventJournal.DomainEntities/EventJournal.DomainEntities.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 EventJournal.DomainEntities enable enable diff --git a/EventJournal.DomainEntities/UserTypes/Intensity.cs b/EventJournal.DomainEntities/UserTypes/Intensity.cs index ca2a62c..46126fe 100644 --- a/EventJournal.DomainEntities/UserTypes/Intensity.cs +++ b/EventJournal.DomainEntities/UserTypes/Intensity.cs @@ -1,4 +1,4 @@ -using EventJournal.DomainEntities.Enums; +using EventJournal.DomainEntities.Enumerations; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; diff --git a/EventJournal.DomainModels/Class1.cs b/EventJournal.DomainModels/Class1.cs new file mode 100644 index 0000000..e27c8fd --- /dev/null +++ b/EventJournal.DomainModels/Class1.cs @@ -0,0 +1,5 @@ +namespace EventJournal.DomainModels { + public class Class1 { + + } +} diff --git a/EventJournal.DomainModels/EventJournal.DomainModels.csproj b/EventJournal.DomainModels/EventJournal.DomainModels.csproj new file mode 100644 index 0000000..125f4c9 --- /dev/null +++ b/EventJournal.DomainModels/EventJournal.DomainModels.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/EventJournal.DomainService/EventJournal.DomainService.csproj b/EventJournal.DomainService/EventJournal.DomainService.csproj index 03d4945..a548a84 100644 --- a/EventJournal.DomainService/EventJournal.DomainService.csproj +++ b/EventJournal.DomainService/EventJournal.DomainService.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 enable enable diff --git a/EventJournal.DomainService/EventService.cs b/EventJournal.DomainService/EventService.cs index 0f604d5..79240cc 100644 --- a/EventJournal.DomainService/EventService.cs +++ b/EventJournal.DomainService/EventService.cs @@ -1,5 +1,37 @@ -namespace EventJournal.DomainService { - public class EventService { +using EventJournal.Data; +using EventJournal.DomainEntities; +namespace EventJournal.DomainService { + public class EventService( + IEventRepository eventRepository, + IDetailRepository detailRepository) : IEventService { + //TODO: refactor to use a generic repository interface if possible + //TODO: refactor to use models(DTOs) instead of entities + + public async Task> GetAllEventsAsync() { + return await eventRepository.GetAllAsync(); + } + public async Task GetEventByIdAsync(Guid resourceId) { + return await eventRepository.GetByResourceIdAsync(resourceId); + } + public async Task AddUpdateEventAsync(Event updatedEvent) { + await eventRepository.AddUpdateAsync(updatedEvent); + } + public async Task DeleteEventAsync(Event entity) { + await eventRepository.DeleteAsync(entity); + } + + public async Task> GetAllDetailsAsync() { + return await detailRepository.GetAllAsync(); + } + public async Task GetDetailByIdAsync(Guid resourceId) { + return await detailRepository.GetByResourceIdAsync(resourceId); + } + public async Task AddUpdateDetailAsync(Detail updatedDetail) { + await detailRepository.AddUpdateAsync(updatedDetail); + } + public async Task DeleteDetailAsync(Detail entity) { + await detailRepository.DeleteAsync(entity); + } } } diff --git a/EventJournal.DomainService/IEventService.cs b/EventJournal.DomainService/IEventService.cs new file mode 100644 index 0000000..3ffa512 --- /dev/null +++ b/EventJournal.DomainService/IEventService.cs @@ -0,0 +1,15 @@ +using EventJournal.DomainEntities; + +namespace EventJournal.DomainService { + public interface IEventService { + Task> GetAllEventsAsync(); + Task GetEventByIdAsync(Guid resourceId); + Task AddUpdateEventAsync(Event updatedEvent); + Task DeleteEventAsync(Event entity); + + Task> GetAllDetailsAsync(); + Task GetDetailByIdAsync(Guid resourceId); + Task AddUpdateDetailAsync(Detail updatedDetail); + Task DeleteDetailAsync(Detail entity); + } +} \ No newline at end of file diff --git a/EventJournal.DomainService/IUserTypeService.cs b/EventJournal.DomainService/IUserTypeService.cs new file mode 100644 index 0000000..34573fe --- /dev/null +++ b/EventJournal.DomainService/IUserTypeService.cs @@ -0,0 +1,4 @@ +namespace EventJournal.DomainService { + public interface IUserTypeService { + } +} \ No newline at end of file diff --git a/EventJournal.DomainService/UserTypeService.cs b/EventJournal.DomainService/UserTypeService.cs index 166803e..7b43d47 100644 --- a/EventJournal.DomainService/UserTypeService.cs +++ b/EventJournal.DomainService/UserTypeService.cs @@ -1,4 +1,51 @@ -namespace EventJournal.DomainService { - public class UserTypeService { +using EventJournal.Data.UserTypeRepositories; +using EventJournal.DomainEntities.UserTypes; + +namespace EventJournal.DomainService { + public class UserTypeService( + IDetailTypeRepository detailRepository, + IEventTypeRepository eventTypeRepository, + IIntensityRepository intensityRepository) : IUserTypeService { + //TODO: refactor to use a generic repository interface if possible + //TODO: refactor to use models(DTOs) instead of entities + + public async Task> GetAllDetailTypesAsync() { + return await detailRepository.GetAllAsync(); + } + public async Task GetDetailTypeByIdAsync(Guid resourceId) { + return await detailRepository.GetByResourceIdAsync(resourceId); + } + public async Task AddUpdateDetailTypeAsync(DetailType updatedDetailType) { + return await detailRepository.AddUpdateAsync(updatedDetailType); + } + public async Task DeleteDetailTypeAsync(DetailType entity) { + await detailRepository.DeleteAsync(entity); + } + + public async Task> GetAllEventTypesAsync() { + return await eventTypeRepository.GetAllAsync(); + } + public async Task GetEventTypeByIdAsync(Guid resourceId) { + return await eventTypeRepository.GetByResourceIdAsync(resourceId); + } + public async Task AddUpdateEventTypeAsync(EventType updatedEventType) { + return await eventTypeRepository.AddUpdateAsync(updatedEventType); + } + public async Task DeleteEventTypeAsync(EventType entity) { + await eventTypeRepository.DeleteAsync(entity); + } + + public async Task> GetAllIntensitiesAsync() { + return await intensityRepository.GetAllAsync(); + } + public async Task GetIntensityByIdAsync(Guid resourceId) { + return await intensityRepository.GetByResourceIdAsync(resourceId); + } + public async Task UpdateIntensityAsync(Intensity updatedIntensity) { + return await intensityRepository.AddUpdateAsync(updatedIntensity); + } + public async Task DeleteIntensityAsync(Intensity entity) { + await intensityRepository.DeleteAsync(entity); + } } } diff --git a/EventJournal.WebAPI/EventJournal.WebAPI.csproj b/EventJournal.WebAPI/EventJournal.WebAPI.csproj index 744fc6a..5beac2d 100644 --- a/EventJournal.WebAPI/EventJournal.WebAPI.csproj +++ b/EventJournal.WebAPI/EventJournal.WebAPI.csproj @@ -1,7 +1,7 @@ - + - net8.0 + net9.0 enable enable EventJournal.WebAPI diff --git a/EventJournal.sln b/EventJournal.sln index a7e4fab..9e1b838 100644 --- a/EventJournal.sln +++ b/EventJournal.sln @@ -19,6 +19,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.Data", "EventJ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.DomainService", "EventJournal.DomainService\EventJournal.DomainService.csproj", "{EB6275FF-4E73-466D-9A44-EFDBF992CA75}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.DomainModels", "EventJournal.DomainModels\EventJournal.DomainModels.csproj", "{153C0741-E492-490C-8C90-FB5805FD0AFE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -45,6 +47,10 @@ Global {EB6275FF-4E73-466D-9A44-EFDBF992CA75}.Debug|Any CPU.Build.0 = Debug|Any CPU {EB6275FF-4E73-466D-9A44-EFDBF992CA75}.Release|Any CPU.ActiveCfg = Release|Any CPU {EB6275FF-4E73-466D-9A44-EFDBF992CA75}.Release|Any CPU.Build.0 = Release|Any CPU + {153C0741-E492-490C-8C90-FB5805FD0AFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {153C0741-E492-490C-8C90-FB5805FD0AFE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {153C0741-E492-490C-8C90-FB5805FD0AFE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {153C0741-E492-490C-8C90-FB5805FD0AFE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/README.md b/README.md index 1de81ae..906dbe9 100644 --- a/README.md +++ b/README.md @@ -38,11 +38,30 @@ Events are the central entity. An `Event` has an `EventType` and one or more `De - Major (water mostly red) (3) - Intense (water completely red) (4) -### TODO +#### Exercise Intensity could be +- Took it easy (0) +- Pushed a bit (1) +- Heart Pumping and light sweating (2) +- Breathing Hard and sweating good (3) +- Hardcore Must take it easy tomorrow (4) -populate Events domain service - manages Events and Details -populate UserType service - manages all the User configurable types. +#### Entity Framework help +EF https://learn.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs=netcore-cli -Event entries should also have user defined tags. +### TODO +- fix all awaits to call ConfigureAwait(false) to avoid deadlocks in UI apps. +- - consider a `ConfigureAwait` extension method that does this automatically. +- c consider adding an async analyzer +- populate UserType service - manages all the User configurable types. +- refactor services to consume DTOs (models) instead of entities directly. +- unit tests for repositories and services. Also base entity code? +- CLI interface to call service methods +- Web API to call service methods +- Add common BootStrap code to be consumed by cli and web api projects +- +### Future Considerations +- basic CRUD web UI +- Integration tests for web api. +- `Event` entries could also have user defined tags. (Maybe `Details` could have tags too?) not sure how to implement tags just yet, maybe a tag entity and table and a tags list table (with an id) and have journal entry track tag-list id From 5a8a43db11cfe9295d4ae185e681341d13670595 Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Sun, 17 Aug 2025 17:47:40 -0400 Subject: [PATCH 14/34] add ansyc analyzers, fix reported async issues --- EventJournal.CLI/EventJournal.CLI.csproj | 4 ++ EventJournal.Data/BaseRepository.cs | 8 ++-- EventJournal.Data/EventJournal.Data.csproj | 4 ++ .../EventJournal.DomainEntities.csproj | 4 ++ .../EventJournal.DomainModels.csproj | 7 +++ .../EventJournal.DomainService.csproj | 7 +++ EventJournal.DomainService/EventService.cs | 32 ++++++------- EventJournal.DomainService/UserTypeService.cs | 48 +++++++++---------- .../EventJournal.WebAPI.csproj | 6 ++- README.md | 8 ++-- 10 files changed, 79 insertions(+), 49 deletions(-) diff --git a/EventJournal.CLI/EventJournal.CLI.csproj b/EventJournal.CLI/EventJournal.CLI.csproj index 6fd7bd3..e56e7cc 100644 --- a/EventJournal.CLI/EventJournal.CLI.csproj +++ b/EventJournal.CLI/EventJournal.CLI.csproj @@ -15,6 +15,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/EventJournal.Data/BaseRepository.cs b/EventJournal.Data/BaseRepository.cs index 51736ba..7bc7fa1 100644 --- a/EventJournal.Data/BaseRepository.cs +++ b/EventJournal.Data/BaseRepository.cs @@ -19,13 +19,13 @@ public async Task> GetAllAsync() { return await table.FindAsync(id).ConfigureAwait(false); } - public async Task GetByResourceIdAsync(Guid resourceId) { - return await table.FirstOrDefaultAsync(t => t.ResourceId == resourceId).ConfigureAwait(false); + public Task GetByResourceIdAsync(Guid resourceId) { + return table.FirstOrDefaultAsync(t => t.ResourceId == resourceId); } - public async Task DeleteAsync(T entity) { + public Task DeleteAsync(T entity) { table.Remove(entity); - await db.SaveChangesAsync().ConfigureAwait(false); + return db.SaveChangesAsync(); } // TODO: this seems like it could/should be an extension method diff --git a/EventJournal.Data/EventJournal.Data.csproj b/EventJournal.Data/EventJournal.Data.csproj index 1ba8487..9b1e539 100644 --- a/EventJournal.Data/EventJournal.Data.csproj +++ b/EventJournal.Data/EventJournal.Data.csproj @@ -12,6 +12,10 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/EventJournal.DomainEntities/EventJournal.DomainEntities.csproj b/EventJournal.DomainEntities/EventJournal.DomainEntities.csproj index 1f5a270..dda6d0f 100644 --- a/EventJournal.DomainEntities/EventJournal.DomainEntities.csproj +++ b/EventJournal.DomainEntities/EventJournal.DomainEntities.csproj @@ -9,6 +9,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/EventJournal.DomainModels/EventJournal.DomainModels.csproj b/EventJournal.DomainModels/EventJournal.DomainModels.csproj index 125f4c9..cbe69ac 100644 --- a/EventJournal.DomainModels/EventJournal.DomainModels.csproj +++ b/EventJournal.DomainModels/EventJournal.DomainModels.csproj @@ -6,4 +6,11 @@ enable + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/EventJournal.DomainService/EventJournal.DomainService.csproj b/EventJournal.DomainService/EventJournal.DomainService.csproj index a548a84..b0566c3 100644 --- a/EventJournal.DomainService/EventJournal.DomainService.csproj +++ b/EventJournal.DomainService/EventJournal.DomainService.csproj @@ -6,6 +6,13 @@ enable + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/EventJournal.DomainService/EventService.cs b/EventJournal.DomainService/EventService.cs index 79240cc..f951467 100644 --- a/EventJournal.DomainService/EventService.cs +++ b/EventJournal.DomainService/EventService.cs @@ -8,30 +8,30 @@ public class EventService( //TODO: refactor to use a generic repository interface if possible //TODO: refactor to use models(DTOs) instead of entities - public async Task> GetAllEventsAsync() { - return await eventRepository.GetAllAsync(); + public Task> GetAllEventsAsync() { + return eventRepository.GetAllAsync(); } - public async Task GetEventByIdAsync(Guid resourceId) { - return await eventRepository.GetByResourceIdAsync(resourceId); + public Task GetEventByIdAsync(Guid resourceId) { + return eventRepository.GetByResourceIdAsync(resourceId); } - public async Task AddUpdateEventAsync(Event updatedEvent) { - await eventRepository.AddUpdateAsync(updatedEvent); + public Task AddUpdateEventAsync(Event updatedEvent) { + return eventRepository.AddUpdateAsync(updatedEvent); } - public async Task DeleteEventAsync(Event entity) { - await eventRepository.DeleteAsync(entity); + public Task DeleteEventAsync(Event entity) { + return eventRepository.DeleteAsync(entity); } - public async Task> GetAllDetailsAsync() { - return await detailRepository.GetAllAsync(); + public Task> GetAllDetailsAsync() { + return detailRepository.GetAllAsync(); } - public async Task GetDetailByIdAsync(Guid resourceId) { - return await detailRepository.GetByResourceIdAsync(resourceId); + public Task GetDetailByIdAsync(Guid resourceId) { + return detailRepository.GetByResourceIdAsync(resourceId); } - public async Task AddUpdateDetailAsync(Detail updatedDetail) { - await detailRepository.AddUpdateAsync(updatedDetail); + public Task AddUpdateDetailAsync(Detail updatedDetail) { + return detailRepository.AddUpdateAsync(updatedDetail); } - public async Task DeleteDetailAsync(Detail entity) { - await detailRepository.DeleteAsync(entity); + public Task DeleteDetailAsync(Detail entity) { + return detailRepository.DeleteAsync(entity); } } } diff --git a/EventJournal.DomainService/UserTypeService.cs b/EventJournal.DomainService/UserTypeService.cs index 7b43d47..8fd27a4 100644 --- a/EventJournal.DomainService/UserTypeService.cs +++ b/EventJournal.DomainService/UserTypeService.cs @@ -9,43 +9,43 @@ public class UserTypeService( //TODO: refactor to use a generic repository interface if possible //TODO: refactor to use models(DTOs) instead of entities - public async Task> GetAllDetailTypesAsync() { - return await detailRepository.GetAllAsync(); + public Task> GetAllDetailTypesAsync() { + return detailRepository.GetAllAsync(); } - public async Task GetDetailTypeByIdAsync(Guid resourceId) { - return await detailRepository.GetByResourceIdAsync(resourceId); + public Task GetDetailTypeByIdAsync(Guid resourceId) { + return detailRepository.GetByResourceIdAsync(resourceId); } - public async Task AddUpdateDetailTypeAsync(DetailType updatedDetailType) { - return await detailRepository.AddUpdateAsync(updatedDetailType); + public Task AddUpdateDetailTypeAsync(DetailType updatedDetailType) { + return detailRepository.AddUpdateAsync(updatedDetailType); } - public async Task DeleteDetailTypeAsync(DetailType entity) { - await detailRepository.DeleteAsync(entity); + public Task DeleteDetailTypeAsync(DetailType entity) { + return detailRepository.DeleteAsync(entity); } - public async Task> GetAllEventTypesAsync() { - return await eventTypeRepository.GetAllAsync(); + public Task> GetAllEventTypesAsync() { + return eventTypeRepository.GetAllAsync(); } - public async Task GetEventTypeByIdAsync(Guid resourceId) { - return await eventTypeRepository.GetByResourceIdAsync(resourceId); + public Task GetEventTypeByIdAsync(Guid resourceId) { + return eventTypeRepository.GetByResourceIdAsync(resourceId); } - public async Task AddUpdateEventTypeAsync(EventType updatedEventType) { - return await eventTypeRepository.AddUpdateAsync(updatedEventType); + public Task AddUpdateEventTypeAsync(EventType updatedEventType) { + return eventTypeRepository.AddUpdateAsync(updatedEventType); } - public async Task DeleteEventTypeAsync(EventType entity) { - await eventTypeRepository.DeleteAsync(entity); + public Task DeleteEventTypeAsync(EventType entity) { + return eventTypeRepository.DeleteAsync(entity); } - public async Task> GetAllIntensitiesAsync() { - return await intensityRepository.GetAllAsync(); + public Task> GetAllIntensitiesAsync() { + return intensityRepository.GetAllAsync(); } - public async Task GetIntensityByIdAsync(Guid resourceId) { - return await intensityRepository.GetByResourceIdAsync(resourceId); + public Task GetIntensityByIdAsync(Guid resourceId) { + return intensityRepository.GetByResourceIdAsync(resourceId); } - public async Task UpdateIntensityAsync(Intensity updatedIntensity) { - return await intensityRepository.AddUpdateAsync(updatedIntensity); + public Task UpdateIntensityAsync(Intensity updatedIntensity) { + return intensityRepository.AddUpdateAsync(updatedIntensity); } - public async Task DeleteIntensityAsync(Intensity entity) { - await intensityRepository.DeleteAsync(entity); + public Task DeleteIntensityAsync(Intensity entity) { + return intensityRepository.DeleteAsync(entity); } } } diff --git a/EventJournal.WebAPI/EventJournal.WebAPI.csproj b/EventJournal.WebAPI/EventJournal.WebAPI.csproj index 5beac2d..0cc48bc 100644 --- a/EventJournal.WebAPI/EventJournal.WebAPI.csproj +++ b/EventJournal.WebAPI/EventJournal.WebAPI.csproj @@ -1,4 +1,4 @@ - + net9.0 @@ -15,6 +15,10 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/README.md b/README.md index 906dbe9..81ba927 100644 --- a/README.md +++ b/README.md @@ -49,10 +49,10 @@ Events are the central entity. An `Event` has an `EventType` and one or more `De EF https://learn.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs=netcore-cli ### TODO -- fix all awaits to call ConfigureAwait(false) to avoid deadlocks in UI apps. -- - consider a `ConfigureAwait` extension method that does this automatically. -- c consider adding an async analyzer -- populate UserType service - manages all the User configurable types. +- fix all awaits to call ConfigureAwait(false) to avoid deadlocks in UI. + - consider a `ConfigureAwait` extension method that does this automatically. + - consider adding an async analyzer + - refactor services to consume DTOs (models) instead of entities directly. - unit tests for repositories and services. Also base entity code? - CLI interface to call service methods From 0a5c8be662caa07667fbd095431d5df6adddd7a2 Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Sun, 17 Aug 2025 18:01:15 -0400 Subject: [PATCH 15/34] a bit more cleanup --- EventJournal.CLI/EventJournal.CLI.csproj | 4 ++++ EventJournal.CLI/Program.cs | 2 +- EventJournal.Data/BaseRepository.cs | 1 - EventJournal.Data/EventJournal.Data.csproj | 4 ++++ .../EventJournal.DomainEntities.csproj | 5 ++++- .../EventJournal.DomainModels.csproj | 4 ++++ .../EventJournal.DomainService.csproj | 4 ++++ EventJournal.WebAPI/EventJournal.WebAPI.csproj | 6 ++++++ README.md | 12 ++++++------ 9 files changed, 33 insertions(+), 9 deletions(-) diff --git a/EventJournal.CLI/EventJournal.CLI.csproj b/EventJournal.CLI/EventJournal.CLI.csproj index e56e7cc..9bc5bfd 100644 --- a/EventJournal.CLI/EventJournal.CLI.csproj +++ b/EventJournal.CLI/EventJournal.CLI.csproj @@ -9,6 +9,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/EventJournal.CLI/Program.cs b/EventJournal.CLI/Program.cs index d3ae0f7..06caea4 100644 --- a/EventJournal.CLI/Program.cs +++ b/EventJournal.CLI/Program.cs @@ -12,7 +12,7 @@ private static void Main(string[] args) { var EventService = services.GetService() ?? throw new Exception("Unable to locate a valid Product Logic module"); var UserTypeServes = services.GetService() ?? throw new Exception("Unable to locate a valid Order Logic module"); - //TODO: move to shared startup.cs + //TODO: move to shared startup.cs and remove this method and remove microsoft.extension.hosting pkg static IServiceProvider CreateServiceCollection() { var servicecollection = new ServiceCollection() .AddDbContext(options => { diff --git a/EventJournal.Data/BaseRepository.cs b/EventJournal.Data/BaseRepository.cs index 7bc7fa1..b02b6b5 100644 --- a/EventJournal.Data/BaseRepository.cs +++ b/EventJournal.Data/BaseRepository.cs @@ -28,7 +28,6 @@ public Task DeleteAsync(T entity) { return db.SaveChangesAsync(); } - // TODO: this seems like it could/should be an extension method public async Task AddUpdateAsync(T source) { ArgumentException.ThrowIfNullOrEmpty(nameof(source)); var existingEntity = await GetByIdAsync(source.Id) diff --git a/EventJournal.Data/EventJournal.Data.csproj b/EventJournal.Data/EventJournal.Data.csproj index 9b1e539..83c9cdf 100644 --- a/EventJournal.Data/EventJournal.Data.csproj +++ b/EventJournal.Data/EventJournal.Data.csproj @@ -7,6 +7,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/EventJournal.DomainEntities/EventJournal.DomainEntities.csproj b/EventJournal.DomainEntities/EventJournal.DomainEntities.csproj index dda6d0f..9ea05fd 100644 --- a/EventJournal.DomainEntities/EventJournal.DomainEntities.csproj +++ b/EventJournal.DomainEntities/EventJournal.DomainEntities.csproj @@ -8,7 +8,10 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/EventJournal.DomainModels/EventJournal.DomainModels.csproj b/EventJournal.DomainModels/EventJournal.DomainModels.csproj index cbe69ac..9bc16ea 100644 --- a/EventJournal.DomainModels/EventJournal.DomainModels.csproj +++ b/EventJournal.DomainModels/EventJournal.DomainModels.csproj @@ -7,6 +7,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/EventJournal.DomainService/EventJournal.DomainService.csproj b/EventJournal.DomainService/EventJournal.DomainService.csproj index b0566c3..a4aa25b 100644 --- a/EventJournal.DomainService/EventJournal.DomainService.csproj +++ b/EventJournal.DomainService/EventJournal.DomainService.csproj @@ -7,6 +7,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/EventJournal.WebAPI/EventJournal.WebAPI.csproj b/EventJournal.WebAPI/EventJournal.WebAPI.csproj index 0cc48bc..71d6689 100644 --- a/EventJournal.WebAPI/EventJournal.WebAPI.csproj +++ b/EventJournal.WebAPI/EventJournal.WebAPI.csproj @@ -10,10 +10,16 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive + + all diff --git a/README.md b/README.md index 81ba927..c454169 100644 --- a/README.md +++ b/README.md @@ -49,18 +49,18 @@ Events are the central entity. An `Event` has an `EventType` and one or more `De EF https://learn.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs=netcore-cli ### TODO -- fix all awaits to call ConfigureAwait(false) to avoid deadlocks in UI. - - consider a `ConfigureAwait` extension method that does this automatically. - - consider adding an async analyzer - - refactor services to consume DTOs (models) instead of entities directly. - unit tests for repositories and services. Also base entity code? - CLI interface to call service methods - Web API to call service methods - Add common BootStrap code to be consumed by cli and web api projects -- + - remove microsoft.extension.hosting pkg where not needed +- move initializer code to an appropriate place + - want to let user get some defaults to start with + - but also wan to use these defaults for testing ### Future Considerations -- basic CRUD web UI +- basic CRUD web UI + - want to add the ability to add new types on the fly as a new event is added - Integration tests for web api. - `Event` entries could also have user defined tags. (Maybe `Details` could have tags too?) not sure how to implement tags just yet, maybe a tag entity and table From 7a3ca602bc1f17e4fae1ce249b2561f48a28acba Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Sun, 17 Aug 2025 19:59:07 -0400 Subject: [PATCH 16/34] move entities to .Data, remove .DomainEntities project, other prep for adding models --- EventJournal.CLI/CLIUtilities.cs | 50 +++++ EventJournal.CLI/EventJournal.CLI.csproj | 1 - EventJournal.CLI/Program.cs | 191 +++++++++++++++++- EventJournal.Data/BaseRepository.cs | 2 +- EventJournal.Data/DatabaseContext.cs | 4 +- EventJournal.Data/DetailRepository.cs | 2 +- .../Entities}/BaseEntity.cs | 19 +- EventJournal.Data/Entities/Detail.cs | 32 +++ .../Entities/Enumerations/SortType.cs | 7 + .../Entities}/Event.cs | 23 ++- .../Entities}/UserTypes/DetailType.cs | 17 +- .../Entities}/UserTypes/EventType.cs | 17 +- .../Entities/UserTypes/Intensity.cs | 38 ++++ EventJournal.Data/EventJournal.Data.csproj | 4 - EventJournal.Data/EventRepository.cs | 2 +- EventJournal.Data/IBaseRepository.cs | 2 +- EventJournal.Data/IDatabaseContext.cs | 4 +- EventJournal.Data/IDetailRepository.cs | 2 +- EventJournal.Data/IEventRepository.cs | 2 +- .../DetailTypeRepository.cs | 3 +- .../EventTypeRepository.cs | 2 +- .../IDetailTypeRepository.cs | 2 +- .../IEventTypeRepository.cs | 2 +- .../IIntensityRepository.cs | 2 +- .../IntensityRepository.cs | 3 +- EventJournal.DomainEntities/Detail.cs | 21 -- .../Enumerations/SortType.cs | 5 - .../EventJournal.DomainEntities.csproj | 21 -- .../UserTypes/Intensity.cs | 26 --- .../{Class1.cs => BaseModel.cs} | 2 +- .../EventJournal.DomainService.csproj | 1 - EventJournal.DomainService/EventService.cs | 2 +- EventJournal.DomainService/IEventService.cs | 2 +- .../IUserTypeService.cs | 16 +- EventJournal.DomainService/UserTypeService.cs | 4 +- EventJournal.sln | 6 - README.md | 4 + 37 files changed, 410 insertions(+), 133 deletions(-) create mode 100644 EventJournal.CLI/CLIUtilities.cs rename {EventJournal.DomainEntities => EventJournal.Data/Entities}/BaseEntity.cs (71%) create mode 100644 EventJournal.Data/Entities/Detail.cs create mode 100644 EventJournal.Data/Entities/Enumerations/SortType.cs rename {EventJournal.DomainEntities => EventJournal.Data/Entities}/Event.cs (53%) rename {EventJournal.DomainEntities => EventJournal.Data/Entities}/UserTypes/DetailType.cs (54%) rename {EventJournal.DomainEntities => EventJournal.Data/Entities}/UserTypes/EventType.cs (76%) create mode 100644 EventJournal.Data/Entities/UserTypes/Intensity.cs delete mode 100644 EventJournal.DomainEntities/Detail.cs delete mode 100644 EventJournal.DomainEntities/Enumerations/SortType.cs delete mode 100644 EventJournal.DomainEntities/EventJournal.DomainEntities.csproj delete mode 100644 EventJournal.DomainEntities/UserTypes/Intensity.cs rename EventJournal.DomainModels/{Class1.cs => BaseModel.cs} (63%) diff --git a/EventJournal.CLI/CLIUtilities.cs b/EventJournal.CLI/CLIUtilities.cs new file mode 100644 index 0000000..d4270aa --- /dev/null +++ b/EventJournal.CLI/CLIUtilities.cs @@ -0,0 +1,50 @@ +namespace EventJournal.CLI { + public static class CLIUtilities { + public static string GetStringFromUser(string prompt) { + string? userInput = null; + + while (string.IsNullOrWhiteSpace(userInput)) { + Console.Write(prompt); + userInput = Console.ReadLine(); + } + return userInput; + } + + public static int GetIntFromUser(string prompt) { + var intFromUser = 0; + + var isInvalidInput = true; + while (isInvalidInput) { + Console.Write(prompt); + isInvalidInput = !int.TryParse(Console.ReadLine(), + System.Globalization.NumberStyles.AllowLeadingWhite + | System.Globalization.NumberStyles.AllowTrailingWhite, + System.Globalization.CultureInfo.CurrentCulture, + out intFromUser); + if (isInvalidInput) { + Console.WriteLine("Must be an integer."); + } + } + + return intFromUser; + } + + public static decimal GetDecimalFromUser(string prompt) { + var decimalFromUser = 0.0M; + var isInvalidInput = true; + while (isInvalidInput) { + Console.Write(prompt); + isInvalidInput = !decimal.TryParse(Console.ReadLine(), + System.Globalization.NumberStyles.AllowLeadingWhite + | System.Globalization.NumberStyles.AllowTrailingWhite + | System.Globalization.NumberStyles.Currency, + System.Globalization.CultureInfo.CurrentCulture, + out decimalFromUser); + if (isInvalidInput) { + Console.WriteLine("Must be a number."); + } + } + return decimalFromUser; + } + } +} diff --git a/EventJournal.CLI/EventJournal.CLI.csproj b/EventJournal.CLI/EventJournal.CLI.csproj index 9bc5bfd..dbc82e0 100644 --- a/EventJournal.CLI/EventJournal.CLI.csproj +++ b/EventJournal.CLI/EventJournal.CLI.csproj @@ -27,7 +27,6 @@ - diff --git a/EventJournal.CLI/Program.cs b/EventJournal.CLI/Program.cs index 06caea4..7b60190 100644 --- a/EventJournal.CLI/Program.cs +++ b/EventJournal.CLI/Program.cs @@ -1,4 +1,5 @@ // See https://aka.ms/new-console-template for more information +using EventJournal.CLI; using EventJournal.Data; using EventJournal.DomainService; using Microsoft.EntityFrameworkCore; @@ -6,12 +7,68 @@ using Microsoft.Extensions.Logging; internal class Program { - private static void Main(string[] args) { + private static async Task Main(string[] args) { var services = CreateServiceCollection(); var EventService = services.GetService() ?? throw new Exception("Unable to locate a valid Product Logic module"); var UserTypeServes = services.GetService() ?? throw new Exception("Unable to locate a valid Order Logic module"); + bool userIsDone = false; + while (!userIsDone) { + //Console.WriteLine("Type '1' to add/update a product."); + //Console.WriteLine("Type '2' to view a product."); + //Console.WriteLine("Type '3' to view products that are in stock."); + //Console.WriteLine("Type '4' to view all products."); + //Console.WriteLine("Type '6' to add/update an order."); + //Console.WriteLine("Type '7' to view an order."); + //Console.WriteLine("Type '8' to view all orders."); + //Console.WriteLine("Type 'a' to add some test data."); + //Console.WriteLine("Type 'x' to delete all data."); + //Console.WriteLine("Type 'q' to quit."); + + //// application will block here waiting for user to press + //var userInput = CLIUtilities.GetStringFromUser("===> ").ToLower() ?? ""; + + //switch (userInput[0]) { + // case 'q': + // userIsDone = true; + // break; + // case '1': + // await AddUpdateEntity(GetEntityFromUser()).ConfigureAwait(false); + // break; + // case '2': + // await ViewProduct().ConfigureAwait(false); + // break; + // case '3': + // await ViewInStockProducts().ConfigureAwait(false); + // break; + // case '4': + // await ViewAllProduct().ConfigureAwait(false); + // break; + // case '5': + + // break; + // case '6': + // await AddUpdateEntity(GetEntityFromUser()).ConfigureAwait(false); + // break; + // case '7': + // await ViewOrder().ConfigureAwait(false); + // break; + // case '8': + // await ViewallOrders().ConfigureAwait(false); + // break; + // case '9': + + // break; + // case 'a': + // await AddTestData(ProductService, OrderService).ConfigureAwait(false); + // break; + // case 'x': + // await DeleteAllData(ProductService, OrderService).ConfigureAwait(false); + // break; + //} + Console.WriteLine("\n===============================\n"); + } //TODO: move to shared startup.cs and remove this method and remove microsoft.extension.hosting pkg static IServiceProvider CreateServiceCollection() { var servicecollection = new ServiceCollection() @@ -35,5 +92,137 @@ static IServiceProvider CreateServiceCollection() { return servicecollection.BuildServiceProvider(); } + //async Task AddTestData(IProductService productService, IOrderService orderService) { + // Console.WriteLine("Adding/Resetting test data."); + // await AddUpdateEntity(new Product { ProductId = 1, Name = "Super Short Leash", Quantity = 10, Price = 1.99M }).ConfigureAwait(false); + // await AddUpdateEntity(new Product { ProductId = 2, Name = "Dry Cat Food", Quantity = 0, Price = 15.99M }).ConfigureAwait(false); + // await AddUpdateEntity(new Product { ProductId = 100, Name = "Designer Leash", Quantity = 1, Price = 99.99M }).ConfigureAwait(false); + // await AddUpdateEntity(new Order { OrderId = 1, OrderDate = DateTime.Now, OrderProducts = { new OrderProduct { ProductId = 100, OrderQuantity = 5, UnitPrice = 99.99M } } }).ConfigureAwait(false); + // await AddUpdateEntity(new Order { OrderId = 2, OrderDate = DateTime.Now, OrderProducts = { new OrderProduct { ProductId = 2, OrderQuantity = 3, UnitPrice = 15.99M } } }).ConfigureAwait(false); + //} + //static async Task DeleteAllData(IProductService productService, IOrderService orderService) { + // var products = await productService.GetProductsAsync().ConfigureAwait(false); + // products.ForEach(async p => await productService.RemoveProductAsync(p)); + + // var orders = await orderService.GetOrdersAsync().ConfigureAwait(false); + // orders.ForEach(async o => await orderService.RemoveOrderAsync(o).ConfigureAwait(false)); + //} + + //static T? GetEntityFromUser() where T : EntityBase { + // var json = CLIUtilities.GetStringFromUser($"Enter {typeof(T)} JSON: "); + // T? entity = json.Deserialize(); + // if (entity == null) { + // Console.WriteLine("Invalid JSON."); + // return null; + // } + // return entity; + //} + + //static bool ValidateEntity(T entity) where T : EntityBase { + // var validationResult = entity.Validate(); + // if (!validationResult.IsValid) { + // Console.WriteLine("Invalid Product data."); + // foreach (var error in validationResult.Errors) { + // Console.WriteLine(error); + // } + // return false; + // } + // return true; + //} + + //async Task AddEntity(T? newEntity) where T : EntityBase { + // if (newEntity == null) { + // Console.WriteLine("Nothing added."); + // return; + // } + // if (!ValidateEntity(newEntity)) { + // return; + // } + + // if (newEntity is Product newProduct) { + // await ProductService.AddProductAsync(newProduct); + // Console.WriteLine($"Added {newProduct.Name}."); + // return; + // } + + // if (newEntity is Order newOrder) { + // await OrderService.AddOrderAsync(newOrder); + // Console.WriteLine($"Added order # {newOrder.OrderId}."); + // } + //} + + //async Task ViewProduct() { + // var name = CLIUtilities.GetStringFromUser("Enter the product name you want to view: "); + // var product = await ProductService.GetProductAsync(name).ConfigureAwait(false); + // if (product == null) { + // Console.WriteLine($"The product '{name}' was not found.\n"); + // return; + // } + // Console.WriteLine(product.Serialize()); + // Console.WriteLine(); + //} + + //async Task ViewOrder() { + // var orderId = CLIUtilities.GetIntFromUser("Enter the order id you want to view: "); + // var order = await OrderService.GetOrderAsync(orderId).ConfigureAwait(false); + // if (order == null) { + // Console.WriteLine($"Order Id {orderId} was not found.\n"); + // return; + // } + // Console.WriteLine(order.ToString()); + // Console.WriteLine(); + //} + + + //async Task ViewInStockProducts() { + // var inStock = await ProductService.GetInStockProductsAsync().ConfigureAwait(false); + // Console.WriteLine("The following products are in stock: "); + // inStock.ForEach(p => Console.WriteLine(p.Serialize())); + //} + + //async Task ViewAllProduct() { + // var products = await ProductService.GetProductsAsync().ConfigureAwait(false); + // products.ForEach(p => Console.WriteLine(p.Serialize())); + //} + + //async Task ViewallOrders() { + // var orders = await OrderService.GetOrdersAsync().ConfigureAwait(false); + // orders.ForEach(o => Console.WriteLine(o.ToString())); + //} + + //async Task AddUpdateEntity(T? entityUpdate) where T : EntityBase { + + // if (entityUpdate == null) { + // Console.WriteLine("Nothing updated."); + // return; + // } + + // if (!ValidateEntity(entityUpdate)) { + // return; + // } + + // if (entityUpdate is Product product) { + // if (!await ProductService.ProductExists(product.ProductId)) { + // Console.WriteLine($"Product not found. Adding."); + // await AddEntity(product); + // return; + // } + // Console.WriteLine($"Updating product Id {product.ProductId}"); + // await ProductService.UpdateProduct(product).ConfigureAwait(false); + // return; + // } + // if (entityUpdate is Order order) { + // //TODO: edit PS10 instructions to include valid json now that OrderProduct is a thing. + // if (!await OrderService.OrderExists(order.OrderId)) { + // Console.WriteLine($"Order not found. Adding."); + // await AddEntity(order); + // return; + // } + // Console.WriteLine($"Updating order Id {order.OrderId}"); + // await OrderService.UpdateOrder(order).ConfigureAwait(false); + // return; + // } + //} + } } \ No newline at end of file diff --git a/EventJournal.Data/BaseRepository.cs b/EventJournal.Data/BaseRepository.cs index b02b6b5..ad661ac 100644 --- a/EventJournal.Data/BaseRepository.cs +++ b/EventJournal.Data/BaseRepository.cs @@ -1,4 +1,4 @@ -using EventJournal.DomainEntities; +using EventJournal.Data.Entities; using Microsoft.EntityFrameworkCore; namespace EventJournal.Data { diff --git a/EventJournal.Data/DatabaseContext.cs b/EventJournal.Data/DatabaseContext.cs index 220c5e3..3c4be20 100644 --- a/EventJournal.Data/DatabaseContext.cs +++ b/EventJournal.Data/DatabaseContext.cs @@ -1,5 +1,5 @@ -using EventJournal.DomainEntities; -using EventJournal.DomainEntities.UserTypes; +using EventJournal.Data.Entities; +using EventJournal.Data.Entities.UserTypes; using Microsoft.EntityFrameworkCore; namespace EventJournal.Data { diff --git a/EventJournal.Data/DetailRepository.cs b/EventJournal.Data/DetailRepository.cs index 5f27a00..2362327 100644 --- a/EventJournal.Data/DetailRepository.cs +++ b/EventJournal.Data/DetailRepository.cs @@ -1,4 +1,4 @@ -using EventJournal.DomainEntities; +using EventJournal.Data.Entities; namespace EventJournal.Data { public class DetailRepository(IDatabaseContext db) : BaseRepository(db, db.Details), IDetailRepository { diff --git a/EventJournal.DomainEntities/BaseEntity.cs b/EventJournal.Data/Entities/BaseEntity.cs similarity index 71% rename from EventJournal.DomainEntities/BaseEntity.cs rename to EventJournal.Data/Entities/BaseEntity.cs index 029a472..a53b9ad 100644 --- a/EventJournal.DomainEntities/BaseEntity.cs +++ b/EventJournal.Data/Entities/BaseEntity.cs @@ -1,14 +1,21 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -namespace EventJournal.DomainEntities { +namespace EventJournal.Data.Entities { public abstract class BaseEntity { - [Required] public DateTime CreatedDate { get; set; } = DateTime.UtcNow; - [Required] public DateTime UpdatedDate { get; set; } = DateTime.UtcNow; + [Required] + public DateTime CreatedDate { get; set; } = DateTime.UtcNow; + + [Required] + public DateTime UpdatedDate { get; set; } = DateTime.UtcNow; + [NotMapped] - [Required] public int Id { get; set; } + [Required] + public int Id { get; set; } + [NotMapped] - [Required] public Guid ResourceId { get; set; } + [Required] + public Guid ResourceId { get; set; } /// /// This method is predominantly for updating an entity based on the values in another entity. @@ -17,7 +24,7 @@ public abstract class BaseEntity { /// /// internal abstract void CopyUserValues(T source); - } + } public static partial class EntityHelpers { public static T UpdateEntity(this T destination, T source) where T : BaseEntity { diff --git a/EventJournal.Data/Entities/Detail.cs b/EventJournal.Data/Entities/Detail.cs new file mode 100644 index 0000000..5096cb5 --- /dev/null +++ b/EventJournal.Data/Entities/Detail.cs @@ -0,0 +1,32 @@ +using EventJournal.Data.Entities.UserTypes; +using System.ComponentModel.DataAnnotations; + +namespace EventJournal.Data.Entities { + public class Detail : BaseEntity { + [Key] + public int DetailId { get { return Id; } set { Id = value; } } + + [Required] + public Guid DetailResourceId { get { return ResourceId; } set { ResourceId = value; } } + + [Required] + public virtual required Event Event { get; set; } + + [Required] + public virtual required DetailType DetailType { get; set; } + + [Required] + public virtual required Intensity Intensity { get; set; } + + [MaxLength(512)] + public string? Notes { get; set; } + + internal override void CopyUserValues(T source) { + var sourceDetail = source as Detail ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(Detail)}"); + Event = sourceDetail.Event; + DetailType = sourceDetail.DetailType; + Intensity = sourceDetail.Intensity; + Notes = sourceDetail.Notes; + } + } +} diff --git a/EventJournal.Data/Entities/Enumerations/SortType.cs b/EventJournal.Data/Entities/Enumerations/SortType.cs new file mode 100644 index 0000000..dd9af3f --- /dev/null +++ b/EventJournal.Data/Entities/Enumerations/SortType.cs @@ -0,0 +1,7 @@ +namespace EventJournal.Data.Entities.Enumerations { + public enum SortType { + Custom, + Ascending, + Descending + } +} diff --git a/EventJournal.DomainEntities/Event.cs b/EventJournal.Data/Entities/Event.cs similarity index 53% rename from EventJournal.DomainEntities/Event.cs rename to EventJournal.Data/Entities/Event.cs index 7d4de32..0273ab0 100644 --- a/EventJournal.DomainEntities/Event.cs +++ b/EventJournal.Data/Entities/Event.cs @@ -1,14 +1,23 @@ -using EventJournal.DomainEntities.UserTypes; +using EventJournal.Data.Entities.UserTypes; using System.ComponentModel.DataAnnotations; -namespace EventJournal.DomainEntities { +namespace EventJournal.Data.Entities { public class Event : BaseEntity { - [Key] public int EventId { get { return Id; } set { Id = value; } } - [Required] public Guid EventResourceId { get { return ResourceId; } set { ResourceId = value; } } - [Required] public EventType Type { get; set; } = EventType.CreateDefaultEventType(); - [Required] public DateTime StartTime { get; set; } = DateTime.Now; + [Key] + public int EventId { get { return Id; } set { Id = value; } } + + [Required] + public Guid EventResourceId { get { return ResourceId; } set { ResourceId = value; } } + + [Required] + public EventType Type { get; set; } = EventType.CreateDefaultEventType(); + + [Required] + public DateTime StartTime { get; set; } = DateTime.Now; public DateTime? EndTime { get; set; } = null; - [MaxLength(500)] public string? Description { get; set; } + + [MaxLength(500)] + public string? Description { get; set; } public IEnumerable Details { get; set; } = []; diff --git a/EventJournal.DomainEntities/UserTypes/DetailType.cs b/EventJournal.Data/Entities/UserTypes/DetailType.cs similarity index 54% rename from EventJournal.DomainEntities/UserTypes/DetailType.cs rename to EventJournal.Data/Entities/UserTypes/DetailType.cs index 6b8ffac..4e07098 100644 --- a/EventJournal.DomainEntities/UserTypes/DetailType.cs +++ b/EventJournal.Data/Entities/UserTypes/DetailType.cs @@ -1,11 +1,18 @@ using System.ComponentModel.DataAnnotations; -namespace EventJournal.DomainEntities.UserTypes { +namespace EventJournal.Data.Entities.UserTypes { public class DetailType : BaseEntity { - [Key] public int DetailTypeId { get { return Id; } set { Id = value; } } - [Required] public Guid DetailTypeResourceId { get { return ResourceId; } set { ResourceId = value; } } - [Required] public required string Name { get; set; } - [MaxLength(500)] public string? Description { get; set; } + [Key] + public int DetailTypeId { get { return Id; } set { Id = value; } } + + [Required] + public Guid DetailTypeResourceId { get { return ResourceId; } set { ResourceId = value; } } + + [Required] + public required string Name { get; set; } + + [MaxLength(500)] + public string? Description { get; set; } public IEnumerable Intensities { get; set; } = []; diff --git a/EventJournal.DomainEntities/UserTypes/EventType.cs b/EventJournal.Data/Entities/UserTypes/EventType.cs similarity index 76% rename from EventJournal.DomainEntities/UserTypes/EventType.cs rename to EventJournal.Data/Entities/UserTypes/EventType.cs index 7366878..8edceff 100644 --- a/EventJournal.DomainEntities/UserTypes/EventType.cs +++ b/EventJournal.Data/Entities/UserTypes/EventType.cs @@ -1,11 +1,18 @@ using System.ComponentModel.DataAnnotations; -namespace EventJournal.DomainEntities.UserTypes { +namespace EventJournal.Data.Entities.UserTypes { public class EventType : BaseEntity { - [Key] public int EventTypeId { get { return Id; } set { Id = value; } } - [Required] public Guid EventTypeResourceId { get { return ResourceId; } set { ResourceId = value; } } - [Required] public required string Name { get; set; } - [MaxLength(500)] public string? Description { get; set; } + [Key] + public int EventTypeId { get { return Id; } set { Id = value; } } + + [Required] + public Guid EventTypeResourceId { get { return ResourceId; } set { ResourceId = value; } } + + [Required] + public required string Name { get; set; } + + [MaxLength(500)] + public string? Description { get; set; } //TODO: this code really belongs in a service or maybe repo public static IEnumerable DefaultEventTypes() { diff --git a/EventJournal.Data/Entities/UserTypes/Intensity.cs b/EventJournal.Data/Entities/UserTypes/Intensity.cs new file mode 100644 index 0000000..a9d02dc --- /dev/null +++ b/EventJournal.Data/Entities/UserTypes/Intensity.cs @@ -0,0 +1,38 @@ +using EventJournal.Data.Entities.Enumerations; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace EventJournal.Data.Entities.UserTypes { + public class Intensity : BaseEntity { + [Key] + public int IntensityId { get { return Id; } set { Id = value; } } + [Required] + public Guid IntensityResourceId { get { return ResourceId; } set { ResourceId = value; } } + + [Required, MaxLength(50)] + public required string Name { get; set; } + + [Required] + public required int Level { get; set; } + + [MaxLength(500)] + public string? Description { get; set; } + + [Required] + public required SortType DefaultSortType { get; set; } + + [ForeignKey(nameof(DetailTypeId))] + [Required] + public required int DetailTypeId { get; set; } + + internal override void CopyUserValues(T source) { + var sourceIntensity = source as Intensity ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(Intensity)}"); + Name = sourceIntensity.Name; + Level = sourceIntensity.Level; + Description = sourceIntensity.Description; + DefaultSortType = sourceIntensity.DefaultSortType; + DetailTypeId = sourceIntensity.DetailTypeId; + } + } + +} diff --git a/EventJournal.Data/EventJournal.Data.csproj b/EventJournal.Data/EventJournal.Data.csproj index 83c9cdf..3309d90 100644 --- a/EventJournal.Data/EventJournal.Data.csproj +++ b/EventJournal.Data/EventJournal.Data.csproj @@ -22,8 +22,4 @@ - - - - diff --git a/EventJournal.Data/EventRepository.cs b/EventJournal.Data/EventRepository.cs index 1a46457..da2475e 100644 --- a/EventJournal.Data/EventRepository.cs +++ b/EventJournal.Data/EventRepository.cs @@ -1,4 +1,4 @@ -using EventJournal.DomainEntities; +using EventJournal.Data.Entities; namespace EventJournal.Data { public class EventRepository(IDatabaseContext db) : BaseRepository(db, db.Events), IEventRepository { diff --git a/EventJournal.Data/IBaseRepository.cs b/EventJournal.Data/IBaseRepository.cs index f7db9a2..127bf35 100644 --- a/EventJournal.Data/IBaseRepository.cs +++ b/EventJournal.Data/IBaseRepository.cs @@ -1,4 +1,4 @@ -using EventJournal.DomainEntities; +using EventJournal.Data.Entities; namespace EventJournal.Data { public interface IBaseRepository where T : BaseEntity { diff --git a/EventJournal.Data/IDatabaseContext.cs b/EventJournal.Data/IDatabaseContext.cs index a948307..8f6ff86 100644 --- a/EventJournal.Data/IDatabaseContext.cs +++ b/EventJournal.Data/IDatabaseContext.cs @@ -1,5 +1,5 @@ -using EventJournal.DomainEntities; -using EventJournal.DomainEntities.UserTypes; +using EventJournal.Data.Entities; +using EventJournal.Data.Entities.UserTypes; using Microsoft.EntityFrameworkCore; namespace EventJournal.Data { diff --git a/EventJournal.Data/IDetailRepository.cs b/EventJournal.Data/IDetailRepository.cs index 1949970..62b91a4 100644 --- a/EventJournal.Data/IDetailRepository.cs +++ b/EventJournal.Data/IDetailRepository.cs @@ -1,4 +1,4 @@ -using EventJournal.DomainEntities; +using EventJournal.Data.Entities; namespace EventJournal.Data { public interface IDetailRepository : IBaseRepository { diff --git a/EventJournal.Data/IEventRepository.cs b/EventJournal.Data/IEventRepository.cs index 6cedaeb..715b829 100644 --- a/EventJournal.Data/IEventRepository.cs +++ b/EventJournal.Data/IEventRepository.cs @@ -1,4 +1,4 @@ -using EventJournal.DomainEntities; +using EventJournal.Data.Entities; namespace EventJournal.Data { public interface IEventRepository : IBaseRepository { diff --git a/EventJournal.Data/UserTypeRepositories/DetailTypeRepository.cs b/EventJournal.Data/UserTypeRepositories/DetailTypeRepository.cs index 31ae222..0c895bf 100644 --- a/EventJournal.Data/UserTypeRepositories/DetailTypeRepository.cs +++ b/EventJournal.Data/UserTypeRepositories/DetailTypeRepository.cs @@ -1,5 +1,4 @@ -using EventJournal.DomainEntities; -using EventJournal.DomainEntities.UserTypes; +using EventJournal.Data.Entities.UserTypes; namespace EventJournal.Data.UserTypeRepositories { public class DetailTypeRepository(IDatabaseContext db) : BaseRepository(db, db.DetailTypes), IDetailTypeRepository { diff --git a/EventJournal.Data/UserTypeRepositories/EventTypeRepository.cs b/EventJournal.Data/UserTypeRepositories/EventTypeRepository.cs index c603a4f..38434ff 100644 --- a/EventJournal.Data/UserTypeRepositories/EventTypeRepository.cs +++ b/EventJournal.Data/UserTypeRepositories/EventTypeRepository.cs @@ -1,4 +1,4 @@ -using EventJournal.DomainEntities.UserTypes; +using EventJournal.Data.Entities.UserTypes; namespace EventJournal.Data.UserTypeRepositories { public class EventTypeRepository(IDatabaseContext db) : BaseRepository(db, db.EventTypes), IEventTypeRepository { diff --git a/EventJournal.Data/UserTypeRepositories/IDetailTypeRepository.cs b/EventJournal.Data/UserTypeRepositories/IDetailTypeRepository.cs index 6454766..782885e 100644 --- a/EventJournal.Data/UserTypeRepositories/IDetailTypeRepository.cs +++ b/EventJournal.Data/UserTypeRepositories/IDetailTypeRepository.cs @@ -1,4 +1,4 @@ -using EventJournal.DomainEntities.UserTypes; +using EventJournal.Data.Entities.UserTypes; namespace EventJournal.Data.UserTypeRepositories { public interface IDetailTypeRepository : IBaseRepository { diff --git a/EventJournal.Data/UserTypeRepositories/IEventTypeRepository.cs b/EventJournal.Data/UserTypeRepositories/IEventTypeRepository.cs index b399343..dec3d7a 100644 --- a/EventJournal.Data/UserTypeRepositories/IEventTypeRepository.cs +++ b/EventJournal.Data/UserTypeRepositories/IEventTypeRepository.cs @@ -1,4 +1,4 @@ -using EventJournal.DomainEntities.UserTypes; +using EventJournal.Data.Entities.UserTypes; namespace EventJournal.Data.UserTypeRepositories { public interface IEventTypeRepository : IBaseRepository { diff --git a/EventJournal.Data/UserTypeRepositories/IIntensityRepository.cs b/EventJournal.Data/UserTypeRepositories/IIntensityRepository.cs index fbed842..2a37373 100644 --- a/EventJournal.Data/UserTypeRepositories/IIntensityRepository.cs +++ b/EventJournal.Data/UserTypeRepositories/IIntensityRepository.cs @@ -1,4 +1,4 @@ -using EventJournal.DomainEntities.UserTypes; +using EventJournal.Data.Entities.UserTypes; namespace EventJournal.Data.UserTypeRepositories { public interface IIntensityRepository : IBaseRepository { diff --git a/EventJournal.Data/UserTypeRepositories/IntensityRepository.cs b/EventJournal.Data/UserTypeRepositories/IntensityRepository.cs index 3b7b53d..ce62f8b 100644 --- a/EventJournal.Data/UserTypeRepositories/IntensityRepository.cs +++ b/EventJournal.Data/UserTypeRepositories/IntensityRepository.cs @@ -1,5 +1,4 @@ -using EventJournal.DomainEntities; -using EventJournal.DomainEntities.UserTypes; +using EventJournal.Data.Entities.UserTypes; namespace EventJournal.Data.UserTypeRepositories { public class IntensityRepository(IDatabaseContext db) : BaseRepository(db, db.Intensities), IIntensityRepository { diff --git a/EventJournal.DomainEntities/Detail.cs b/EventJournal.DomainEntities/Detail.cs deleted file mode 100644 index bb8f4ba..0000000 --- a/EventJournal.DomainEntities/Detail.cs +++ /dev/null @@ -1,21 +0,0 @@ -using EventJournal.DomainEntities.UserTypes; -using System.ComponentModel.DataAnnotations; - -namespace EventJournal.DomainEntities { - public class Detail : BaseEntity{ - [Key] public int DetailId { get { return Id; } set { Id = value; } } - [Required] public Guid DetailResourceId { get { return ResourceId; } set { ResourceId = value; } } - [Required] public virtual required Event Event { get; set; } - [Required] public virtual required DetailType DetailType { get; set; } - [Required] public virtual required Intensity Intensity { get; set; } - [MaxLength(512)] public string? Notes { get; set; } - - internal override void CopyUserValues(T source) { - var sourceDetail = source as Detail ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(Detail)}"); - Event = sourceDetail.Event; - DetailType = sourceDetail.DetailType; - Intensity = sourceDetail.Intensity; - Notes = sourceDetail.Notes; - } - } -} diff --git a/EventJournal.DomainEntities/Enumerations/SortType.cs b/EventJournal.DomainEntities/Enumerations/SortType.cs deleted file mode 100644 index 94c2f20..0000000 --- a/EventJournal.DomainEntities/Enumerations/SortType.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace EventJournal.DomainEntities.Enumerations { - public enum SortType { - Custom, Ascending, Descending - } -} diff --git a/EventJournal.DomainEntities/EventJournal.DomainEntities.csproj b/EventJournal.DomainEntities/EventJournal.DomainEntities.csproj deleted file mode 100644 index 9ea05fd..0000000 --- a/EventJournal.DomainEntities/EventJournal.DomainEntities.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - net9.0 - EventJournal.DomainEntities - enable - enable - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - diff --git a/EventJournal.DomainEntities/UserTypes/Intensity.cs b/EventJournal.DomainEntities/UserTypes/Intensity.cs deleted file mode 100644 index 46126fe..0000000 --- a/EventJournal.DomainEntities/UserTypes/Intensity.cs +++ /dev/null @@ -1,26 +0,0 @@ -using EventJournal.DomainEntities.Enumerations; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace EventJournal.DomainEntities.UserTypes { - public class Intensity : BaseEntity { - [Key] public int IntensityId { get { return Id; } set { Id = value; } } - [Required] public Guid IntensityResourceId { get { return ResourceId; } set { ResourceId = value; } } - [Required, MaxLength(50)] public required string Name { get; set; } - [Required] public required int Level { get; set; } - [MaxLength(500)] public string? Description { get; set; } - [Required] public required SortType DefaultSortType { get; set; } - [ForeignKey(nameof(DetailTypeId))] - [Required] public required int DetailTypeId { get; set; } - - internal override void CopyUserValues(T source) { - var sourceIntensity = source as Intensity ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(Intensity)}"); - Name = sourceIntensity.Name; - Level = sourceIntensity.Level; - Description = sourceIntensity.Description; - DefaultSortType = sourceIntensity.DefaultSortType; - DetailTypeId = sourceIntensity.DetailTypeId; - } - } - -} diff --git a/EventJournal.DomainModels/Class1.cs b/EventJournal.DomainModels/BaseModel.cs similarity index 63% rename from EventJournal.DomainModels/Class1.cs rename to EventJournal.DomainModels/BaseModel.cs index e27c8fd..f236179 100644 --- a/EventJournal.DomainModels/Class1.cs +++ b/EventJournal.DomainModels/BaseModel.cs @@ -1,5 +1,5 @@ namespace EventJournal.DomainModels { - public class Class1 { + public class BaseModel { } } diff --git a/EventJournal.DomainService/EventJournal.DomainService.csproj b/EventJournal.DomainService/EventJournal.DomainService.csproj index a4aa25b..a86800b 100644 --- a/EventJournal.DomainService/EventJournal.DomainService.csproj +++ b/EventJournal.DomainService/EventJournal.DomainService.csproj @@ -19,7 +19,6 @@ - diff --git a/EventJournal.DomainService/EventService.cs b/EventJournal.DomainService/EventService.cs index f951467..9cc775e 100644 --- a/EventJournal.DomainService/EventService.cs +++ b/EventJournal.DomainService/EventService.cs @@ -1,5 +1,5 @@ using EventJournal.Data; -using EventJournal.DomainEntities; +using EventJournal.Data.Entities; namespace EventJournal.DomainService { public class EventService( diff --git a/EventJournal.DomainService/IEventService.cs b/EventJournal.DomainService/IEventService.cs index 3ffa512..a4dcbfb 100644 --- a/EventJournal.DomainService/IEventService.cs +++ b/EventJournal.DomainService/IEventService.cs @@ -1,4 +1,4 @@ -using EventJournal.DomainEntities; +using EventJournal.Data.Entities; namespace EventJournal.DomainService { public interface IEventService { diff --git a/EventJournal.DomainService/IUserTypeService.cs b/EventJournal.DomainService/IUserTypeService.cs index 34573fe..9d0281d 100644 --- a/EventJournal.DomainService/IUserTypeService.cs +++ b/EventJournal.DomainService/IUserTypeService.cs @@ -1,4 +1,18 @@ -namespace EventJournal.DomainService { +using EventJournal.Data.Entities.UserTypes; + +namespace EventJournal.DomainService { public interface IUserTypeService { + Task AddUpdateDetailTypeAsync(DetailType updatedDetailType); + Task AddUpdateEventTypeAsync(EventType updatedEventType); + Task DeleteDetailTypeAsync(DetailType entity); + Task DeleteEventTypeAsync(EventType entity); + Task DeleteIntensityAsync(Intensity entity); + Task> GetAllDetailTypesAsync(); + Task> GetAllEventTypesAsync(); + Task> GetAllIntensitiesAsync(); + Task GetDetailTypeByIdAsync(Guid resourceId); + Task GetEventTypeByIdAsync(Guid resourceId); + Task GetIntensityByIdAsync(Guid resourceId); + Task UpdateIntensityAsync(Intensity updatedIntensity); } } \ No newline at end of file diff --git a/EventJournal.DomainService/UserTypeService.cs b/EventJournal.DomainService/UserTypeService.cs index 8fd27a4..159be5d 100644 --- a/EventJournal.DomainService/UserTypeService.cs +++ b/EventJournal.DomainService/UserTypeService.cs @@ -1,5 +1,5 @@ -using EventJournal.Data.UserTypeRepositories; -using EventJournal.DomainEntities.UserTypes; +using EventJournal.Data.Entities.UserTypes; +using EventJournal.Data.UserTypeRepositories; namespace EventJournal.DomainService { public class UserTypeService( diff --git a/EventJournal.sln b/EventJournal.sln index 9e1b838..e6ee4ea 100644 --- a/EventJournal.sln +++ b/EventJournal.sln @@ -5,8 +5,6 @@ VisualStudioVersion = 17.12.35521.163 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.CLI", "EventJournal.CLI\EventJournal.CLI.csproj", "{D0E93737-A883-4020-9C99-E6C1D70D1237}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.DomainEntities", "EventJournal.DomainEntities\EventJournal.DomainEntities.csproj", "{2F884BE1-F88D-45F7-A3E0-480FCA94541A}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{994FEE9A-A85B-4AEC-894B-CD7FEDF3FC09}" ProjectSection(SolutionItems) = preProject LICENSE = LICENSE @@ -31,10 +29,6 @@ Global {D0E93737-A883-4020-9C99-E6C1D70D1237}.Debug|Any CPU.Build.0 = Debug|Any CPU {D0E93737-A883-4020-9C99-E6C1D70D1237}.Release|Any CPU.ActiveCfg = Release|Any CPU {D0E93737-A883-4020-9C99-E6C1D70D1237}.Release|Any CPU.Build.0 = Release|Any CPU - {2F884BE1-F88D-45F7-A3E0-480FCA94541A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2F884BE1-F88D-45F7-A3E0-480FCA94541A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2F884BE1-F88D-45F7-A3E0-480FCA94541A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2F884BE1-F88D-45F7-A3E0-480FCA94541A}.Release|Any CPU.Build.0 = Release|Any CPU {AFC2B8DD-6CDE-450F-9181-93B2B67983D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AFC2B8DD-6CDE-450F-9181-93B2B67983D7}.Debug|Any CPU.Build.0 = Debug|Any CPU {AFC2B8DD-6CDE-450F-9181-93B2B67983D7}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/README.md b/README.md index c454169..19d794d 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,9 @@ Events are the central entity. An `Event` has an `EventType` and one or more `De EF https://learn.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs=netcore-cli ### TODO +- !!!! fix entity framework + - Entities moved name spaces so we likely have to remove migrations and start all over + - need to finish models first - refactor services to consume DTOs (models) instead of entities directly. - unit tests for repositories and services. Also base entity code? - CLI interface to call service methods @@ -58,6 +61,7 @@ EF https://learn.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs - move initializer code to an appropriate place - want to let user get some defaults to start with - but also wan to use these defaults for testing +- Entity validators ### Future Considerations - basic CRUD web UI - want to add the ability to add new types on the fly as a new event is added From f7560a7592edef83df5ca767cd0ec43f9c6a1533 Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Sun, 17 Aug 2025 22:37:22 -0400 Subject: [PATCH 17/34] Add Automapper and convert services to consume/return DTOs --- EventJournal.CLI/Program.cs | 4 +- EventJournal.Data/AssemblyInfo.cs | 2 + .../Entities/Enumerations/SortType.cs | 1 + .../Entities/UserTypes/EventType.cs | 3 +- EventJournal.DomainModels/BaseDto.cs | 29 ++++++++ EventJournal.DomainModels/BaseModel.cs | 5 -- EventJournal.DomainModels/DetailDto.cs | 28 ++++++++ .../Enumerations/SortType.cs | 8 +++ EventJournal.DomainModels/EventDto.cs | 29 ++++++++ ...s.csproj => EventJournal.DomainDto.csproj} | 0 .../UserTypes/DetailTypeDto.cs | 21 ++++++ .../UserTypes/EventTypeDto.cs | 37 ++++++++++ .../UserTypes/IntensityDto.cs | 35 ++++++++++ .../DomainMapperProfile.cs | 17 +++++ .../EventJournal.DomainService.csproj | 2 + EventJournal.DomainService/EventService.cs | 52 ++++++++------ .../Exceptions/ResourceNotFoundException.cs | 23 ++++++ EventJournal.DomainService/IEventService.cs | 18 ++--- .../IUserTypeService.cs | 30 ++++---- EventJournal.DomainService/UserTypeService.cs | 70 +++++++++++-------- EventJournal.sln | 5 +- 21 files changed, 339 insertions(+), 80 deletions(-) create mode 100644 EventJournal.Data/AssemblyInfo.cs create mode 100644 EventJournal.DomainModels/BaseDto.cs delete mode 100644 EventJournal.DomainModels/BaseModel.cs create mode 100644 EventJournal.DomainModels/DetailDto.cs create mode 100644 EventJournal.DomainModels/Enumerations/SortType.cs create mode 100644 EventJournal.DomainModels/EventDto.cs rename EventJournal.DomainModels/{EventJournal.DomainModels.csproj => EventJournal.DomainDto.csproj} (100%) create mode 100644 EventJournal.DomainModels/UserTypes/DetailTypeDto.cs create mode 100644 EventJournal.DomainModels/UserTypes/EventTypeDto.cs create mode 100644 EventJournal.DomainModels/UserTypes/IntensityDto.cs create mode 100644 EventJournal.DomainService/DomainMapperProfile.cs create mode 100644 EventJournal.DomainService/Exceptions/ResourceNotFoundException.cs diff --git a/EventJournal.CLI/Program.cs b/EventJournal.CLI/Program.cs index 7b60190..ebf43f3 100644 --- a/EventJournal.CLI/Program.cs +++ b/EventJournal.CLI/Program.cs @@ -1,5 +1,4 @@ // See https://aka.ms/new-console-template for more information -using EventJournal.CLI; using EventJournal.Data; using EventJournal.DomainService; using Microsoft.EntityFrameworkCore; @@ -87,7 +86,8 @@ static IServiceProvider CreateServiceCollection() { options.TimestampFormat = "HH:mm:ss.fff "; options.ColorBehavior = Microsoft.Extensions.Logging.Console.LoggerColorBehavior.Enabled; }); - }); + }) + .AddAutoMapper(cfg => { }, typeof(DomainMapperProfile)); return servicecollection.BuildServiceProvider(); diff --git a/EventJournal.Data/AssemblyInfo.cs b/EventJournal.Data/AssemblyInfo.cs new file mode 100644 index 0000000..bc9f759 --- /dev/null +++ b/EventJournal.Data/AssemblyInfo.cs @@ -0,0 +1,2 @@ +using System.Runtime.CompilerServices; +[assembly: InternalsVisibleTo("EventJournal.DomainService")] diff --git a/EventJournal.Data/Entities/Enumerations/SortType.cs b/EventJournal.Data/Entities/Enumerations/SortType.cs index dd9af3f..127eaa4 100644 --- a/EventJournal.Data/Entities/Enumerations/SortType.cs +++ b/EventJournal.Data/Entities/Enumerations/SortType.cs @@ -1,4 +1,5 @@ namespace EventJournal.Data.Entities.Enumerations { + //TODO: why duplicate this enum? (other is in .DomainDto.Enumerations ) public enum SortType { Custom, Ascending, diff --git a/EventJournal.Data/Entities/UserTypes/EventType.cs b/EventJournal.Data/Entities/UserTypes/EventType.cs index 8edceff..d20a781 100644 --- a/EventJournal.Data/Entities/UserTypes/EventType.cs +++ b/EventJournal.Data/Entities/UserTypes/EventType.cs @@ -14,7 +14,7 @@ public class EventType : BaseEntity { [MaxLength(500)] public string? Description { get; set; } - //TODO: this code really belongs in a service or maybe repo + //TODO: does this belong here?? public static IEnumerable DefaultEventTypes() { return [ CreateDefaultEventType(), @@ -25,6 +25,7 @@ public static IEnumerable DefaultEventTypes() { ]; } + //TODO: does this belong here?? public static EventType CreateDefaultEventType() { return new EventType { EventTypeResourceId = Guid.NewGuid(), Name = "Random Event", Description = "Use for tracking random things like onset of pain, headache, or whatever that isn't directly associated with a specific even type." }; } diff --git a/EventJournal.DomainModels/BaseDto.cs b/EventJournal.DomainModels/BaseDto.cs new file mode 100644 index 0000000..6151ef1 --- /dev/null +++ b/EventJournal.DomainModels/BaseDto.cs @@ -0,0 +1,29 @@ +namespace EventJournal.DomainDto { + public abstract class BaseDto { + + public DateTime CreatedDate { get; set; } = DateTime.UtcNow; + + public DateTime UpdatedDate { get; set; } = DateTime.UtcNow; + + public Guid ResourceId { get; set; } + + /// + /// This method is predominantly for updating an entity based on the values in another entity. + /// Copy user values (ids and audit info (created, last modified, etc) should NOT be copied! + /// + /// + /// + internal abstract void CopyUserValues(T source); + + } + + //TODO: is this needed? + public static partial class DtoHelpers { + public static T UpdateEntity(this T destination, T source) where T : BaseDto { + destination.CopyUserValues(source); + destination.CreatedDate = source.CreatedDate; + destination.UpdatedDate = DateTime.UtcNow; + return destination; + } + } +} \ No newline at end of file diff --git a/EventJournal.DomainModels/BaseModel.cs b/EventJournal.DomainModels/BaseModel.cs deleted file mode 100644 index f236179..0000000 --- a/EventJournal.DomainModels/BaseModel.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace EventJournal.DomainModels { - public class BaseModel { - - } -} diff --git a/EventJournal.DomainModels/DetailDto.cs b/EventJournal.DomainModels/DetailDto.cs new file mode 100644 index 0000000..4befb9f --- /dev/null +++ b/EventJournal.DomainModels/DetailDto.cs @@ -0,0 +1,28 @@ +using EventJournal.DomainDto.UserTypes; +using System.ComponentModel.DataAnnotations; + +namespace EventJournal.DomainDto { + public class DetailDto : BaseDto { + public Guid DetailResourceId { get { return ResourceId; } set { ResourceId = value; } } + + [Required] + public virtual required EventDto Event { get; set; } + + [Required] + public virtual required DetailTypeDto DetailType { get; set; } + + [Required] + public virtual required IntensityDto Intensity { get; set; } + + [MaxLength(512)] + public string? Notes { get; set; } + + internal override void CopyUserValues(T source) { + var sourceDetail = source as DetailDto ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(DetailDto)}"); + Event = sourceDetail.Event; + DetailType = sourceDetail.DetailType; + Intensity = sourceDetail.Intensity; + Notes = sourceDetail.Notes; + } + } +} diff --git a/EventJournal.DomainModels/Enumerations/SortType.cs b/EventJournal.DomainModels/Enumerations/SortType.cs new file mode 100644 index 0000000..2758382 --- /dev/null +++ b/EventJournal.DomainModels/Enumerations/SortType.cs @@ -0,0 +1,8 @@ +namespace EventJournal.DomainDto.Enumerations { + //TODO: why duplicate this enum? (other is in .Data.Entities.Enumerations) + public enum SortType { + Custom, + Ascending, + Descending + } +} diff --git a/EventJournal.DomainModels/EventDto.cs b/EventJournal.DomainModels/EventDto.cs new file mode 100644 index 0000000..0a09b95 --- /dev/null +++ b/EventJournal.DomainModels/EventDto.cs @@ -0,0 +1,29 @@ +using EventJournal.DomainDto.UserTypes; +using System.ComponentModel.DataAnnotations; + +namespace EventJournal.DomainDto { + public class EventDto : BaseDto { + public Guid EventResourceId { get { return ResourceId; } set { ResourceId = value; } } + + [Required] + public EventTypeDto Type { get; set; } = EventTypeDto.CreateDefaultEventType(); + + [Required] + public DateTime StartTime { get; set; } = DateTime.Now; + public DateTime? EndTime { get; set; } = null; + + [MaxLength(500)] + public string? Description { get; set; } + + public IEnumerable Details { get; set; } = []; + + internal override void CopyUserValues(T source) { + var soruceEvent = source as EventDto ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(EventDto)}"); + Type = soruceEvent.Type; + StartTime = soruceEvent.StartTime; + EndTime = soruceEvent.EndTime; + Description = soruceEvent.Description; + Details = soruceEvent.Details; + } + } +} diff --git a/EventJournal.DomainModels/EventJournal.DomainModels.csproj b/EventJournal.DomainModels/EventJournal.DomainDto.csproj similarity index 100% rename from EventJournal.DomainModels/EventJournal.DomainModels.csproj rename to EventJournal.DomainModels/EventJournal.DomainDto.csproj diff --git a/EventJournal.DomainModels/UserTypes/DetailTypeDto.cs b/EventJournal.DomainModels/UserTypes/DetailTypeDto.cs new file mode 100644 index 0000000..ef7a7c1 --- /dev/null +++ b/EventJournal.DomainModels/UserTypes/DetailTypeDto.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; + +namespace EventJournal.DomainDto.UserTypes { + public class DetailTypeDto : BaseDto { + public Guid DetailTypeResourceId { get { return ResourceId; } set { ResourceId = value; } } + + [Required] + public required string Name { get; set; } + + [MaxLength(500)] + public string? Description { get; set; } + + public IEnumerable Intensities { get; set; } = []; + + internal override void CopyUserValues(T source) { + var sourceDetailType = source as DetailTypeDto ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(DetailTypeDto)}"); + Name = sourceDetailType.Name; + Description = sourceDetailType.Description; + } + } +} diff --git a/EventJournal.DomainModels/UserTypes/EventTypeDto.cs b/EventJournal.DomainModels/UserTypes/EventTypeDto.cs new file mode 100644 index 0000000..7727598 --- /dev/null +++ b/EventJournal.DomainModels/UserTypes/EventTypeDto.cs @@ -0,0 +1,37 @@ +using System.ComponentModel.DataAnnotations; + +namespace EventJournal.DomainDto.UserTypes { + public class EventTypeDto : BaseDto { + public Guid EventTypeResourceId { get { return ResourceId; } set { ResourceId = value; } } + + [Required] + public required string Name { get; set; } + + [MaxLength(500)] + public string? Description { get; set; } + + //TODO: does this belong here?? + public static IEnumerable DefaultEventTypes() { + return [ + CreateDefaultEventType(), + new EventTypeDto { EventTypeResourceId = Guid.NewGuid(), Name = "Exercise" }, + new EventTypeDto { EventTypeResourceId = Guid.NewGuid(), Name = "Bathroom Visit" }, + new EventTypeDto { EventTypeResourceId = Guid.NewGuid(), Name = "Food Consumption" }, + new EventTypeDto { EventTypeResourceId = Guid.NewGuid(), Name = "WeighIn" }, + ]; + } + + //TODO: does this belong here?? + public static EventTypeDto CreateDefaultEventType() { + return new EventTypeDto { EventTypeResourceId = Guid.NewGuid(), Name = "Random Event", Description = "Use for tracking random things like onset of pain, headache, or whatever that isn't directly associated with a specific even type." }; + } + + internal override void CopyUserValues(T source) { + var sourceEventType = source as EventTypeDto ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(EventTypeDto)}"); + Name = sourceEventType.Name; + Description = sourceEventType.Description; + } + } + +} + diff --git a/EventJournal.DomainModels/UserTypes/IntensityDto.cs b/EventJournal.DomainModels/UserTypes/IntensityDto.cs new file mode 100644 index 0000000..cb06daf --- /dev/null +++ b/EventJournal.DomainModels/UserTypes/IntensityDto.cs @@ -0,0 +1,35 @@ +using EventJournal.DomainDto.Enumerations; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace EventJournal.DomainDto.UserTypes { + public class IntensityDto : BaseDto { + public Guid IntensityResourceId { get { return ResourceId; } set { ResourceId = value; } } + + [Required, MaxLength(50)] + public required string Name { get; set; } + + [Required] + public required int Level { get; set; } + + [MaxLength(500)] + public string? Description { get; set; } + + [Required] + public required SortType DefaultSortType { get; set; } + + [ForeignKey(nameof(DetailTypeId))] + [Required] + public required int DetailTypeId { get; set; } + + internal override void CopyUserValues(T source) { + var sourceIntensity = source as IntensityDto ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(IntensityDto)}"); + Name = sourceIntensity.Name; + Level = sourceIntensity.Level; + Description = sourceIntensity.Description; + DefaultSortType = sourceIntensity.DefaultSortType; + DetailTypeId = sourceIntensity.DetailTypeId; + } + } +} + diff --git a/EventJournal.DomainService/DomainMapperProfile.cs b/EventJournal.DomainService/DomainMapperProfile.cs new file mode 100644 index 0000000..8337903 --- /dev/null +++ b/EventJournal.DomainService/DomainMapperProfile.cs @@ -0,0 +1,17 @@ +using AutoMapper; +using EventJournal.Data.Entities; +using EventJournal.Data.Entities.UserTypes; +using EventJournal.DomainDto; +using EventJournal.DomainDto.UserTypes; + +namespace EventJournal.DomainService { + public class DomainMapperProfile : Profile { + public DomainMapperProfile() { + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + } + } +} diff --git a/EventJournal.DomainService/EventJournal.DomainService.csproj b/EventJournal.DomainService/EventJournal.DomainService.csproj index a86800b..d1f7aed 100644 --- a/EventJournal.DomainService/EventJournal.DomainService.csproj +++ b/EventJournal.DomainService/EventJournal.DomainService.csproj @@ -11,6 +11,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -19,6 +20,7 @@ + diff --git a/EventJournal.DomainService/EventService.cs b/EventJournal.DomainService/EventService.cs index 9cc775e..fe7f679 100644 --- a/EventJournal.DomainService/EventService.cs +++ b/EventJournal.DomainService/EventService.cs @@ -1,37 +1,47 @@ -using EventJournal.Data; +using AutoMapper; +using EventJournal.Data; using EventJournal.Data.Entities; +using EventJournal.DomainDto; +using EventJournal.DomainService.Exceptions; namespace EventJournal.DomainService { public class EventService( - IEventRepository eventRepository, - IDetailRepository detailRepository) : IEventService { - //TODO: refactor to use a generic repository interface if possible - //TODO: refactor to use models(DTOs) instead of entities + IEventRepository eventRepository, + IDetailRepository detailRepository, + IMapper mapper) : IEventService { - public Task> GetAllEventsAsync() { - return eventRepository.GetAllAsync(); + public async Task> GetAllEventsAsync() { + return mapper.Map>(await eventRepository.GetAllAsync().ConfigureAwait(false)); } - public Task GetEventByIdAsync(Guid resourceId) { - return eventRepository.GetByResourceIdAsync(resourceId); + public async Task GetEventByIdAsync(Guid resourceId) { + return mapper.Map(await eventRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false)); } - public Task AddUpdateEventAsync(Event updatedEvent) { - return eventRepository.AddUpdateAsync(updatedEvent); + public async Task AddUpdateEventAsync(EventDto dto) { + ArgumentNullException.ThrowIfNull(dto); + //TODO: additional validations? + return mapper.Map(await eventRepository.AddUpdateAsync(mapper.Map(dto)).ConfigureAwait(false)); } - public Task DeleteEventAsync(Event entity) { - return eventRepository.DeleteAsync(entity); + public async Task DeleteEventAsync(Guid resourceId) { + var entity = await eventRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false); + ResourceNotFoundException.ThrowIfNull(entity, $"Event with resource id {resourceId} not found"); + await eventRepository.DeleteAsync(entity).ConfigureAwait(false); } - public Task> GetAllDetailsAsync() { - return detailRepository.GetAllAsync(); + public async Task> GetAllDetailsAsync() { + return mapper.Map>(await detailRepository.GetAllAsync().ConfigureAwait(false)); } - public Task GetDetailByIdAsync(Guid resourceId) { - return detailRepository.GetByResourceIdAsync(resourceId); + public async Task GetDetailByIdAsync(Guid resourceId) { + return mapper.Map(await detailRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false)); } - public Task AddUpdateDetailAsync(Detail updatedDetail) { - return detailRepository.AddUpdateAsync(updatedDetail); + public async Task AddUpdateDetailAsync(DetailDto dto) { + ArgumentNullException.ThrowIfNull(dto); + //TODO: additional validations? + return mapper.Map(await detailRepository.AddUpdateAsync(mapper.Map(dto)).ConfigureAwait(false)); } - public Task DeleteDetailAsync(Detail entity) { - return detailRepository.DeleteAsync(entity); + public async Task DeleteDetailAsync(Guid resourceId) { + var entity = await detailRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false); + ResourceNotFoundException.ThrowIfNull(entity, $"Event with resource id {resourceId} not found"); + await detailRepository.DeleteAsync(entity).ConfigureAwait(false); } } } diff --git a/EventJournal.DomainService/Exceptions/ResourceNotFoundException.cs b/EventJournal.DomainService/Exceptions/ResourceNotFoundException.cs new file mode 100644 index 0000000..ada6d69 --- /dev/null +++ b/EventJournal.DomainService/Exceptions/ResourceNotFoundException.cs @@ -0,0 +1,23 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace EventJournal.DomainService.Exceptions { + public class ResourceNotFoundException : Exception { + public ResourceNotFoundException() { } + + public ResourceNotFoundException(string message) : base(message) { + } + + public ResourceNotFoundException(string? message, Exception? innerException) : base(message, innerException) { + } + + public static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) { + if (argument is null) { + Throw(paramName); + } + } + + [DoesNotReturn] + internal static void Throw(string? paramName) => throw new ArgumentNullException(paramName); + } +} diff --git a/EventJournal.DomainService/IEventService.cs b/EventJournal.DomainService/IEventService.cs index a4dcbfb..dee664e 100644 --- a/EventJournal.DomainService/IEventService.cs +++ b/EventJournal.DomainService/IEventService.cs @@ -1,15 +1,15 @@ -using EventJournal.Data.Entities; +using EventJournal.DomainDto; namespace EventJournal.DomainService { public interface IEventService { - Task> GetAllEventsAsync(); - Task GetEventByIdAsync(Guid resourceId); - Task AddUpdateEventAsync(Event updatedEvent); - Task DeleteEventAsync(Event entity); + Task> GetAllEventsAsync(); + Task GetEventByIdAsync(Guid resourceId); + Task AddUpdateEventAsync(EventDto dto); + Task DeleteEventAsync(Guid resourceId); - Task> GetAllDetailsAsync(); - Task GetDetailByIdAsync(Guid resourceId); - Task AddUpdateDetailAsync(Detail updatedDetail); - Task DeleteDetailAsync(Detail entity); + Task> GetAllDetailsAsync(); + Task GetDetailByIdAsync(Guid resourceId); + Task AddUpdateDetailAsync(DetailDto dto); + Task DeleteDetailAsync(Guid resourceId); } } \ No newline at end of file diff --git a/EventJournal.DomainService/IUserTypeService.cs b/EventJournal.DomainService/IUserTypeService.cs index 9d0281d..77e4159 100644 --- a/EventJournal.DomainService/IUserTypeService.cs +++ b/EventJournal.DomainService/IUserTypeService.cs @@ -1,18 +1,22 @@ -using EventJournal.Data.Entities.UserTypes; +using EventJournal.DomainDto.UserTypes; namespace EventJournal.DomainService { public interface IUserTypeService { - Task AddUpdateDetailTypeAsync(DetailType updatedDetailType); - Task AddUpdateEventTypeAsync(EventType updatedEventType); - Task DeleteDetailTypeAsync(DetailType entity); - Task DeleteEventTypeAsync(EventType entity); - Task DeleteIntensityAsync(Intensity entity); - Task> GetAllDetailTypesAsync(); - Task> GetAllEventTypesAsync(); - Task> GetAllIntensitiesAsync(); - Task GetDetailTypeByIdAsync(Guid resourceId); - Task GetEventTypeByIdAsync(Guid resourceId); - Task GetIntensityByIdAsync(Guid resourceId); - Task UpdateIntensityAsync(Intensity updatedIntensity); + + + Task> GetAllDetailTypesAsync(); + Task GetDetailTypeByIdAsync(Guid resourceId); + Task AddUpdateDetailTypeAsync(DetailTypeDto dto); + Task DeleteDetailTypeAsync(Guid resourceId); + + Task> GetAllEventTypesAsync(); + Task GetEventTypeByIdAsync(Guid resourceId); + Task AddUpdateEventTypeAsync(EventTypeDto dto); + Task DeleteEventTypeAsync(Guid resourceId); + + Task> GetAllIntensitiesAsync(); + Task GetIntensityByIdAsync(Guid resourceId); + Task AddUpdateIntensityAsync(IntensityDto dto); + Task DeleteIntensityAsync(Guid resourceId); } } \ No newline at end of file diff --git a/EventJournal.DomainService/UserTypeService.cs b/EventJournal.DomainService/UserTypeService.cs index 159be5d..7126791 100644 --- a/EventJournal.DomainService/UserTypeService.cs +++ b/EventJournal.DomainService/UserTypeService.cs @@ -1,51 +1,65 @@ -using EventJournal.Data.Entities.UserTypes; +using AutoMapper; +using EventJournal.Data.Entities.UserTypes; using EventJournal.Data.UserTypeRepositories; +using EventJournal.DomainDto.UserTypes; +using EventJournal.DomainService.Exceptions; namespace EventJournal.DomainService { public class UserTypeService( IDetailTypeRepository detailRepository, IEventTypeRepository eventTypeRepository, - IIntensityRepository intensityRepository) : IUserTypeService { - //TODO: refactor to use a generic repository interface if possible - //TODO: refactor to use models(DTOs) instead of entities + IIntensityRepository intensityRepository, + Mapper mapper) : IUserTypeService { - public Task> GetAllDetailTypesAsync() { - return detailRepository.GetAllAsync(); + public async Task> GetAllDetailTypesAsync() { + return mapper.Map>(await detailRepository.GetAllAsync().ConfigureAwait(false)); } - public Task GetDetailTypeByIdAsync(Guid resourceId) { - return detailRepository.GetByResourceIdAsync(resourceId); + public async Task GetDetailTypeByIdAsync(Guid resourceId) { + return mapper.Map(await detailRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false)); } - public Task AddUpdateDetailTypeAsync(DetailType updatedDetailType) { - return detailRepository.AddUpdateAsync(updatedDetailType); + public async Task AddUpdateDetailTypeAsync(DetailTypeDto dto) { + ArgumentNullException.ThrowIfNull(dto); + //TODO: additional validations? + return mapper.Map(await detailRepository.AddUpdateAsync(mapper.Map(dto)).ConfigureAwait(false)); } - public Task DeleteDetailTypeAsync(DetailType entity) { - return detailRepository.DeleteAsync(entity); + public async Task DeleteDetailTypeAsync(Guid resourceId) { + var entity = await detailRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false); + ResourceNotFoundException.ThrowIfNull(entity, $"Event with resource id {resourceId} not found"); + await detailRepository.DeleteAsync(entity); } - public Task> GetAllEventTypesAsync() { - return eventTypeRepository.GetAllAsync(); + public async Task> GetAllEventTypesAsync() { + return mapper.Map>(await eventTypeRepository.GetAllAsync().ConfigureAwait(false)); } - public Task GetEventTypeByIdAsync(Guid resourceId) { - return eventTypeRepository.GetByResourceIdAsync(resourceId); + public async Task GetEventTypeByIdAsync(Guid resourceId) { + return mapper.Map(await eventTypeRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false)); } - public Task AddUpdateEventTypeAsync(EventType updatedEventType) { - return eventTypeRepository.AddUpdateAsync(updatedEventType); + public async Task AddUpdateEventTypeAsync(EventTypeDto dto) { + ArgumentNullException.ThrowIfNull(dto); + //TODO: additional validations? + return mapper.Map(await eventTypeRepository.AddUpdateAsync(mapper.Map(dto)).ConfigureAwait(false)); } - public Task DeleteEventTypeAsync(EventType entity) { - return eventTypeRepository.DeleteAsync(entity); + public async Task DeleteEventTypeAsync(Guid resourceId) { + var entity = await eventTypeRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false); + ResourceNotFoundException.ThrowIfNull(entity, $"Event with resource id {resourceId} not found"); + await eventTypeRepository.DeleteAsync(entity); } - public Task> GetAllIntensitiesAsync() { - return intensityRepository.GetAllAsync(); + public async Task> GetAllIntensitiesAsync() { + return mapper.Map>(await intensityRepository.GetAllAsync().ConfigureAwait(false)); } - public Task GetIntensityByIdAsync(Guid resourceId) { - return intensityRepository.GetByResourceIdAsync(resourceId); + public async Task GetIntensityByIdAsync(Guid resourceId) { + return mapper.Map(await intensityRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false)); } - public Task UpdateIntensityAsync(Intensity updatedIntensity) { - return intensityRepository.AddUpdateAsync(updatedIntensity); + public async Task AddUpdateIntensityAsync(IntensityDto dto) { + ArgumentNullException.ThrowIfNull(dto); + //TODO: additional validations? + return mapper.Map(await intensityRepository.AddUpdateAsync(mapper.Map(dto)).ConfigureAwait(false)); } - public Task DeleteIntensityAsync(Intensity entity) { - return intensityRepository.DeleteAsync(entity); + public async Task DeleteIntensityAsync(Guid resourceId) { + var entity = await intensityRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false); + ResourceNotFoundException.ThrowIfNull(entity, $"Event with resource id {resourceId} not found"); + await intensityRepository.DeleteAsync(entity); } } } diff --git a/EventJournal.sln b/EventJournal.sln index e6ee4ea..614baca 100644 --- a/EventJournal.sln +++ b/EventJournal.sln @@ -17,7 +17,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.Data", "EventJ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.DomainService", "EventJournal.DomainService\EventJournal.DomainService.csproj", "{EB6275FF-4E73-466D-9A44-EFDBF992CA75}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.DomainModels", "EventJournal.DomainModels\EventJournal.DomainModels.csproj", "{153C0741-E492-490C-8C90-FB5805FD0AFE}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.DomainDto", "EventJournal.DomainModels\EventJournal.DomainDto.csproj", "{153C0741-E492-490C-8C90-FB5805FD0AFE}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -49,4 +49,7 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {50C11A8B-C5F5-42E2-BCBB-F012EE87507C} + EndGlobalSection EndGlobal From 29a3782cc796e76d79725f474c8d42b3a2bd775e Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Sun, 17 Aug 2025 23:02:50 -0400 Subject: [PATCH 18/34] fix ef migrations to use entities from new file locations --- ...250818030132_newInitialCreate.Designer.cs} | 36 +++++++++---------- ....cs => 20250818030132_newInitialCreate.cs} | 2 +- .../DatabaseContextModelSnapshot.cs | 32 ++++++++--------- 3 files changed, 35 insertions(+), 35 deletions(-) rename EventJournal.Data/Migrations/{20250622211818_InitialCreate.Designer.cs => 20250818030132_newInitialCreate.Designer.cs} (83%) rename EventJournal.Data/Migrations/{20250622211818_InitialCreate.cs => 20250818030132_newInitialCreate.cs} (99%) diff --git a/EventJournal.Data/Migrations/20250622211818_InitialCreate.Designer.cs b/EventJournal.Data/Migrations/20250818030132_newInitialCreate.Designer.cs similarity index 83% rename from EventJournal.Data/Migrations/20250622211818_InitialCreate.Designer.cs rename to EventJournal.Data/Migrations/20250818030132_newInitialCreate.Designer.cs index 9c75a05..fc02127 100644 --- a/EventJournal.Data/Migrations/20250622211818_InitialCreate.Designer.cs +++ b/EventJournal.Data/Migrations/20250818030132_newInitialCreate.Designer.cs @@ -11,16 +11,16 @@ namespace EventJournal.Data.Migrations { [DbContext(typeof(DatabaseContext))] - [Migration("20250622211818_InitialCreate")] - partial class InitialCreate + [Migration("20250818030132_newInitialCreate")] + partial class newInitialCreate { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.3"); + modelBuilder.HasAnnotation("ProductVersion", "9.0.8"); - modelBuilder.Entity("EventJournal.DomainEntities.Detail", b => + modelBuilder.Entity("EventJournal.Data.Entities.Detail", b => { b.Property("DetailId") .ValueGeneratedOnAdd() @@ -59,7 +59,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("Details"); }); - modelBuilder.Entity("EventJournal.DomainEntities.Event", b => + modelBuilder.Entity("EventJournal.Data.Entities.Event", b => { b.Property("EventId") .ValueGeneratedOnAdd() @@ -94,7 +94,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("Events"); }); - modelBuilder.Entity("EventJournal.DomainEntities.UserTypes.DetailType", b => + modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.DetailType", b => { b.Property("DetailTypeId") .ValueGeneratedOnAdd() @@ -122,7 +122,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("DetailTypes"); }); - modelBuilder.Entity("EventJournal.DomainEntities.UserTypes.EventType", b => + modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.EventType", b => { b.Property("EventTypeId") .ValueGeneratedOnAdd() @@ -150,7 +150,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("EventTypes"); }); - modelBuilder.Entity("EventJournal.DomainEntities.UserTypes.Intensity", b => + modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.Intensity", b => { b.Property("IntensityId") .ValueGeneratedOnAdd() @@ -190,21 +190,21 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("Intensities"); }); - modelBuilder.Entity("EventJournal.DomainEntities.Detail", b => + modelBuilder.Entity("EventJournal.Data.Entities.Detail", b => { - b.HasOne("EventJournal.DomainEntities.UserTypes.DetailType", "DetailType") + b.HasOne("EventJournal.Data.Entities.UserTypes.DetailType", "DetailType") .WithMany() .HasForeignKey("DetailTypeId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("EventJournal.DomainEntities.Event", "Event") + b.HasOne("EventJournal.Data.Entities.Event", "Event") .WithMany("Details") .HasForeignKey("EventId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("EventJournal.DomainEntities.UserTypes.Intensity", "Intensity") + b.HasOne("EventJournal.Data.Entities.UserTypes.Intensity", "Intensity") .WithMany() .HasForeignKey("IntensityId") .OnDelete(DeleteBehavior.Cascade) @@ -217,9 +217,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("Intensity"); }); - modelBuilder.Entity("EventJournal.DomainEntities.Event", b => + modelBuilder.Entity("EventJournal.Data.Entities.Event", b => { - b.HasOne("EventJournal.DomainEntities.UserTypes.EventType", "Type") + b.HasOne("EventJournal.Data.Entities.UserTypes.EventType", "Type") .WithMany() .HasForeignKey("TypeEventTypeId") .OnDelete(DeleteBehavior.Cascade) @@ -228,21 +228,21 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("Type"); }); - modelBuilder.Entity("EventJournal.DomainEntities.UserTypes.Intensity", b => + modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.Intensity", b => { - b.HasOne("EventJournal.DomainEntities.UserTypes.DetailType", null) + b.HasOne("EventJournal.Data.Entities.UserTypes.DetailType", null) .WithMany("Intensities") .HasForeignKey("DetailTypeId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); - modelBuilder.Entity("EventJournal.DomainEntities.Event", b => + modelBuilder.Entity("EventJournal.Data.Entities.Event", b => { b.Navigation("Details"); }); - modelBuilder.Entity("EventJournal.DomainEntities.UserTypes.DetailType", b => + modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.DetailType", b => { b.Navigation("Intensities"); }); diff --git a/EventJournal.Data/Migrations/20250622211818_InitialCreate.cs b/EventJournal.Data/Migrations/20250818030132_newInitialCreate.cs similarity index 99% rename from EventJournal.Data/Migrations/20250622211818_InitialCreate.cs rename to EventJournal.Data/Migrations/20250818030132_newInitialCreate.cs index 175582c..d60ac4d 100644 --- a/EventJournal.Data/Migrations/20250622211818_InitialCreate.cs +++ b/EventJournal.Data/Migrations/20250818030132_newInitialCreate.cs @@ -6,7 +6,7 @@ namespace EventJournal.Data.Migrations { /// - public partial class InitialCreate : Migration + public partial class newInitialCreate : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) diff --git a/EventJournal.Data/Migrations/DatabaseContextModelSnapshot.cs b/EventJournal.Data/Migrations/DatabaseContextModelSnapshot.cs index 34999c8..8736a5d 100644 --- a/EventJournal.Data/Migrations/DatabaseContextModelSnapshot.cs +++ b/EventJournal.Data/Migrations/DatabaseContextModelSnapshot.cs @@ -15,9 +15,9 @@ partial class DatabaseContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.3"); + modelBuilder.HasAnnotation("ProductVersion", "9.0.8"); - modelBuilder.Entity("EventJournal.DomainEntities.Detail", b => + modelBuilder.Entity("EventJournal.Data.Entities.Detail", b => { b.Property("DetailId") .ValueGeneratedOnAdd() @@ -56,7 +56,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Details"); }); - modelBuilder.Entity("EventJournal.DomainEntities.Event", b => + modelBuilder.Entity("EventJournal.Data.Entities.Event", b => { b.Property("EventId") .ValueGeneratedOnAdd() @@ -91,7 +91,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Events"); }); - modelBuilder.Entity("EventJournal.DomainEntities.UserTypes.DetailType", b => + modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.DetailType", b => { b.Property("DetailTypeId") .ValueGeneratedOnAdd() @@ -119,7 +119,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("DetailTypes"); }); - modelBuilder.Entity("EventJournal.DomainEntities.UserTypes.EventType", b => + modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.EventType", b => { b.Property("EventTypeId") .ValueGeneratedOnAdd() @@ -147,7 +147,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("EventTypes"); }); - modelBuilder.Entity("EventJournal.DomainEntities.UserTypes.Intensity", b => + modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.Intensity", b => { b.Property("IntensityId") .ValueGeneratedOnAdd() @@ -187,21 +187,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Intensities"); }); - modelBuilder.Entity("EventJournal.DomainEntities.Detail", b => + modelBuilder.Entity("EventJournal.Data.Entities.Detail", b => { - b.HasOne("EventJournal.DomainEntities.UserTypes.DetailType", "DetailType") + b.HasOne("EventJournal.Data.Entities.UserTypes.DetailType", "DetailType") .WithMany() .HasForeignKey("DetailTypeId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("EventJournal.DomainEntities.Event", "Event") + b.HasOne("EventJournal.Data.Entities.Event", "Event") .WithMany("Details") .HasForeignKey("EventId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("EventJournal.DomainEntities.UserTypes.Intensity", "Intensity") + b.HasOne("EventJournal.Data.Entities.UserTypes.Intensity", "Intensity") .WithMany() .HasForeignKey("IntensityId") .OnDelete(DeleteBehavior.Cascade) @@ -214,9 +214,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Intensity"); }); - modelBuilder.Entity("EventJournal.DomainEntities.Event", b => + modelBuilder.Entity("EventJournal.Data.Entities.Event", b => { - b.HasOne("EventJournal.DomainEntities.UserTypes.EventType", "Type") + b.HasOne("EventJournal.Data.Entities.UserTypes.EventType", "Type") .WithMany() .HasForeignKey("TypeEventTypeId") .OnDelete(DeleteBehavior.Cascade) @@ -225,21 +225,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Type"); }); - modelBuilder.Entity("EventJournal.DomainEntities.UserTypes.Intensity", b => + modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.Intensity", b => { - b.HasOne("EventJournal.DomainEntities.UserTypes.DetailType", null) + b.HasOne("EventJournal.Data.Entities.UserTypes.DetailType", null) .WithMany("Intensities") .HasForeignKey("DetailTypeId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); - modelBuilder.Entity("EventJournal.DomainEntities.Event", b => + modelBuilder.Entity("EventJournal.Data.Entities.Event", b => { b.Navigation("Details"); }); - modelBuilder.Entity("EventJournal.DomainEntities.UserTypes.DetailType", b => + modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.DetailType", b => { b.Navigation("Intensities"); }); From 0e33ca22d95ff6b6dbcccf549e91761d617a4053 Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Mon, 18 Aug 2025 01:52:38 -0400 Subject: [PATCH 19/34] pdates to entities, attempt to add test data via cli (wip), mappers appear to work! --- EventJournal.Data/BaseRepository.cs | 2 +- EventJournal.Data/Entities/Detail.cs | 4 +- EventJournal.Data/Entities/Event.cs | 4 +- .../Entities/UserTypes/DetailType.cs | 2 +- .../Entities/UserTypes/EventType.cs | 17 ++----- .../Entities/UserTypes/Intensity.cs | 4 +- EventJournal.Data/IBaseRepository.cs | 2 +- ...250818052438_newInitialCreate.Designer.cs} | 48 +++++++++---------- ....cs => 20250818052438_newInitialCreate.cs} | 40 ++++++++-------- .../DatabaseContextModelSnapshot.cs | 46 +++++++++--------- 10 files changed, 79 insertions(+), 90 deletions(-) rename EventJournal.Data/Migrations/{20250818030132_newInitialCreate.Designer.cs => 20250818052438_newInitialCreate.Designer.cs} (89%) rename EventJournal.Data/Migrations/{20250818030132_newInitialCreate.cs => 20250818052438_newInitialCreate.cs} (82%) diff --git a/EventJournal.Data/BaseRepository.cs b/EventJournal.Data/BaseRepository.cs index ad661ac..0edea45 100644 --- a/EventJournal.Data/BaseRepository.cs +++ b/EventJournal.Data/BaseRepository.cs @@ -11,7 +11,7 @@ internal async Task AddAsync(T entity) { return row.Entity; } - public async Task> GetAllAsync() { + public async Task> GetAllAsync() { return await table.ToListAsync().ConfigureAwait(false); } diff --git a/EventJournal.Data/Entities/Detail.cs b/EventJournal.Data/Entities/Detail.cs index 5096cb5..7ed0901 100644 --- a/EventJournal.Data/Entities/Detail.cs +++ b/EventJournal.Data/Entities/Detail.cs @@ -4,10 +4,10 @@ namespace EventJournal.Data.Entities { public class Detail : BaseEntity { [Key] - public int DetailId { get { return Id; } set { Id = value; } } + public new int Id { get { return base.Id; } set { base.Id = value; } } [Required] - public Guid DetailResourceId { get { return ResourceId; } set { ResourceId = value; } } + public new Guid ResourceId { get { return base.ResourceId; } set { base.ResourceId = value; } } [Required] public virtual required Event Event { get; set; } diff --git a/EventJournal.Data/Entities/Event.cs b/EventJournal.Data/Entities/Event.cs index 0273ab0..49e4a4e 100644 --- a/EventJournal.Data/Entities/Event.cs +++ b/EventJournal.Data/Entities/Event.cs @@ -4,10 +4,10 @@ namespace EventJournal.Data.Entities { public class Event : BaseEntity { [Key] - public int EventId { get { return Id; } set { Id = value; } } + public new int Id { get { return base.Id; } set { base.Id = value; } } [Required] - public Guid EventResourceId { get { return ResourceId; } set { ResourceId = value; } } + public new Guid ResourceId { get { return base.ResourceId; } set { base.ResourceId = value; } } [Required] public EventType Type { get; set; } = EventType.CreateDefaultEventType(); diff --git a/EventJournal.Data/Entities/UserTypes/DetailType.cs b/EventJournal.Data/Entities/UserTypes/DetailType.cs index 4e07098..cc3a852 100644 --- a/EventJournal.Data/Entities/UserTypes/DetailType.cs +++ b/EventJournal.Data/Entities/UserTypes/DetailType.cs @@ -14,7 +14,7 @@ public class DetailType : BaseEntity { [MaxLength(500)] public string? Description { get; set; } - public IEnumerable Intensities { get; set; } = []; + public IEnumerable AllowedIntensities { get; set; } = []; internal override void CopyUserValues(T source) { var sourceDetailType = source as DetailType ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(DetailType)}"); diff --git a/EventJournal.Data/Entities/UserTypes/EventType.cs b/EventJournal.Data/Entities/UserTypes/EventType.cs index d20a781..49e5efc 100644 --- a/EventJournal.Data/Entities/UserTypes/EventType.cs +++ b/EventJournal.Data/Entities/UserTypes/EventType.cs @@ -3,10 +3,10 @@ namespace EventJournal.Data.Entities.UserTypes { public class EventType : BaseEntity { [Key] - public int EventTypeId { get { return Id; } set { Id = value; } } + public new int Id { get { return base.Id; } set { base.Id = value; } } [Required] - public Guid EventTypeResourceId { get { return ResourceId; } set { ResourceId = value; } } + public new Guid ResourceId { get { return base.ResourceId; } set { base.ResourceId = value; } } [Required] public required string Name { get; set; } @@ -14,20 +14,9 @@ public class EventType : BaseEntity { [MaxLength(500)] public string? Description { get; set; } - //TODO: does this belong here?? - public static IEnumerable DefaultEventTypes() { - return [ - CreateDefaultEventType(), - new EventType { EventTypeResourceId = Guid.NewGuid(), Name = "Exercise" }, - new EventType { EventTypeResourceId = Guid.NewGuid(), Name = "Bathroom Visit" }, - new EventType { EventTypeResourceId = Guid.NewGuid(), Name = "Food Consumption" }, - new EventType { EventTypeResourceId = Guid.NewGuid(), Name = "WeighIn" }, - ]; - } - //TODO: does this belong here?? public static EventType CreateDefaultEventType() { - return new EventType { EventTypeResourceId = Guid.NewGuid(), Name = "Random Event", Description = "Use for tracking random things like onset of pain, headache, or whatever that isn't directly associated with a specific even type." }; + return new EventType { ResourceId = Guid.NewGuid(), Name = "Random Event", Description = "Use for tracking random things like onset of pain, headache, or whatever that isn't directly associated with a specific even type." }; } internal override void CopyUserValues(T source) { diff --git a/EventJournal.Data/Entities/UserTypes/Intensity.cs b/EventJournal.Data/Entities/UserTypes/Intensity.cs index a9d02dc..337d0ec 100644 --- a/EventJournal.Data/Entities/UserTypes/Intensity.cs +++ b/EventJournal.Data/Entities/UserTypes/Intensity.cs @@ -5,9 +5,9 @@ namespace EventJournal.Data.Entities.UserTypes { public class Intensity : BaseEntity { [Key] - public int IntensityId { get { return Id; } set { Id = value; } } + public new int Id { get { return base.Id; } set { base.Id = value; } } [Required] - public Guid IntensityResourceId { get { return ResourceId; } set { ResourceId = value; } } + public new Guid ResourceId { get { return base.ResourceId; } set { base.ResourceId = value; } } [Required, MaxLength(50)] public required string Name { get; set; } diff --git a/EventJournal.Data/IBaseRepository.cs b/EventJournal.Data/IBaseRepository.cs index 127bf35..3f31764 100644 --- a/EventJournal.Data/IBaseRepository.cs +++ b/EventJournal.Data/IBaseRepository.cs @@ -2,7 +2,7 @@ namespace EventJournal.Data { public interface IBaseRepository where T : BaseEntity { - Task> GetAllAsync(); + Task> GetAllAsync(); Task GetByResourceIdAsync(Guid resourceId); Task AddUpdateAsync(T source); Task DeleteAsync(T entity); diff --git a/EventJournal.Data/Migrations/20250818030132_newInitialCreate.Designer.cs b/EventJournal.Data/Migrations/20250818052438_newInitialCreate.Designer.cs similarity index 89% rename from EventJournal.Data/Migrations/20250818030132_newInitialCreate.Designer.cs rename to EventJournal.Data/Migrations/20250818052438_newInitialCreate.Designer.cs index fc02127..cbd9fc7 100644 --- a/EventJournal.Data/Migrations/20250818030132_newInitialCreate.Designer.cs +++ b/EventJournal.Data/Migrations/20250818052438_newInitialCreate.Designer.cs @@ -11,7 +11,7 @@ namespace EventJournal.Data.Migrations { [DbContext(typeof(DatabaseContext))] - [Migration("20250818030132_newInitialCreate")] + [Migration("20250818052438_newInitialCreate")] partial class newInitialCreate { /// @@ -22,16 +22,13 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("EventJournal.Data.Entities.Detail", b => { - b.Property("DetailId") + b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); b.Property("CreatedDate") .HasColumnType("TEXT"); - b.Property("DetailResourceId") - .HasColumnType("TEXT"); - b.Property("DetailTypeId") .HasColumnType("INTEGER"); @@ -45,10 +42,13 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasMaxLength(512) .HasColumnType("TEXT"); + b.Property("ResourceId") + .HasColumnType("TEXT"); + b.Property("UpdatedDate") .HasColumnType("TEXT"); - b.HasKey("DetailId"); + b.HasKey("Id"); b.HasIndex("DetailTypeId"); @@ -61,7 +61,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("EventJournal.Data.Entities.Event", b => { - b.Property("EventId") + b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); @@ -75,21 +75,21 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("EndTime") .HasColumnType("TEXT"); - b.Property("EventResourceId") + b.Property("ResourceId") .HasColumnType("TEXT"); b.Property("StartTime") .HasColumnType("TEXT"); - b.Property("TypeEventTypeId") + b.Property("TypeId") .HasColumnType("INTEGER"); b.Property("UpdatedDate") .HasColumnType("TEXT"); - b.HasKey("EventId"); + b.HasKey("Id"); - b.HasIndex("TypeEventTypeId"); + b.HasIndex("TypeId"); b.ToTable("Events"); }); @@ -124,7 +124,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.EventType", b => { - b.Property("EventTypeId") + b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); @@ -135,24 +135,24 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasMaxLength(500) .HasColumnType("TEXT"); - b.Property("EventTypeResourceId") - .HasColumnType("TEXT"); - b.Property("Name") .IsRequired() .HasColumnType("TEXT"); + b.Property("ResourceId") + .HasColumnType("TEXT"); + b.Property("UpdatedDate") .HasColumnType("TEXT"); - b.HasKey("EventTypeId"); + b.HasKey("Id"); b.ToTable("EventTypes"); }); modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.Intensity", b => { - b.Property("IntensityId") + b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); @@ -169,9 +169,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("DetailTypeId") .HasColumnType("INTEGER"); - b.Property("IntensityResourceId") - .HasColumnType("TEXT"); - b.Property("Level") .HasColumnType("INTEGER"); @@ -180,10 +177,13 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasMaxLength(50) .HasColumnType("TEXT"); + b.Property("ResourceId") + .HasColumnType("TEXT"); + b.Property("UpdatedDate") .HasColumnType("TEXT"); - b.HasKey("IntensityId"); + b.HasKey("Id"); b.HasIndex("DetailTypeId"); @@ -221,7 +221,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { b.HasOne("EventJournal.Data.Entities.UserTypes.EventType", "Type") .WithMany() - .HasForeignKey("TypeEventTypeId") + .HasForeignKey("TypeId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -231,7 +231,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.Intensity", b => { b.HasOne("EventJournal.Data.Entities.UserTypes.DetailType", null) - .WithMany("Intensities") + .WithMany("AllowedIntensities") .HasForeignKey("DetailTypeId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -244,7 +244,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.DetailType", b => { - b.Navigation("Intensities"); + b.Navigation("AllowedIntensities"); }); #pragma warning restore 612, 618 } diff --git a/EventJournal.Data/Migrations/20250818030132_newInitialCreate.cs b/EventJournal.Data/Migrations/20250818052438_newInitialCreate.cs similarity index 82% rename from EventJournal.Data/Migrations/20250818030132_newInitialCreate.cs rename to EventJournal.Data/Migrations/20250818052438_newInitialCreate.cs index d60ac4d..eb23be1 100644 --- a/EventJournal.Data/Migrations/20250818030132_newInitialCreate.cs +++ b/EventJournal.Data/Migrations/20250818052438_newInitialCreate.cs @@ -32,9 +32,9 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "EventTypes", columns: table => new { - EventTypeId = table.Column(type: "INTEGER", nullable: false) + Id = table.Column(type: "INTEGER", nullable: false) .Annotation("Sqlite:Autoincrement", true), - EventTypeResourceId = table.Column(type: "TEXT", nullable: false), + ResourceId = table.Column(type: "TEXT", nullable: false), Name = table.Column(type: "TEXT", nullable: false), Description = table.Column(type: "TEXT", maxLength: 500, nullable: true), CreatedDate = table.Column(type: "TEXT", nullable: false), @@ -42,16 +42,16 @@ protected override void Up(MigrationBuilder migrationBuilder) }, constraints: table => { - table.PrimaryKey("PK_EventTypes", x => x.EventTypeId); + table.PrimaryKey("PK_EventTypes", x => x.Id); }); migrationBuilder.CreateTable( name: "Intensities", columns: table => new { - IntensityId = table.Column(type: "INTEGER", nullable: false) + Id = table.Column(type: "INTEGER", nullable: false) .Annotation("Sqlite:Autoincrement", true), - IntensityResourceId = table.Column(type: "TEXT", nullable: false), + ResourceId = table.Column(type: "TEXT", nullable: false), Name = table.Column(type: "TEXT", maxLength: 50, nullable: false), Level = table.Column(type: "INTEGER", nullable: false), Description = table.Column(type: "TEXT", maxLength: 500, nullable: true), @@ -62,7 +62,7 @@ protected override void Up(MigrationBuilder migrationBuilder) }, constraints: table => { - table.PrimaryKey("PK_Intensities", x => x.IntensityId); + table.PrimaryKey("PK_Intensities", x => x.Id); table.ForeignKey( name: "FK_Intensities_DetailTypes_DetailTypeId", column: x => x.DetailTypeId, @@ -75,10 +75,10 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "Events", columns: table => new { - EventId = table.Column(type: "INTEGER", nullable: false) + Id = table.Column(type: "INTEGER", nullable: false) .Annotation("Sqlite:Autoincrement", true), - EventResourceId = table.Column(type: "TEXT", nullable: false), - TypeEventTypeId = table.Column(type: "INTEGER", nullable: false), + ResourceId = table.Column(type: "TEXT", nullable: false), + TypeId = table.Column(type: "INTEGER", nullable: false), StartTime = table.Column(type: "TEXT", nullable: false), EndTime = table.Column(type: "TEXT", nullable: true), Description = table.Column(type: "TEXT", maxLength: 500, nullable: true), @@ -87,12 +87,12 @@ protected override void Up(MigrationBuilder migrationBuilder) }, constraints: table => { - table.PrimaryKey("PK_Events", x => x.EventId); + table.PrimaryKey("PK_Events", x => x.Id); table.ForeignKey( - name: "FK_Events_EventTypes_TypeEventTypeId", - column: x => x.TypeEventTypeId, + name: "FK_Events_EventTypes_TypeId", + column: x => x.TypeId, principalTable: "EventTypes", - principalColumn: "EventTypeId", + principalColumn: "Id", onDelete: ReferentialAction.Cascade); }); @@ -100,9 +100,9 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "Details", columns: table => new { - DetailId = table.Column(type: "INTEGER", nullable: false) + Id = table.Column(type: "INTEGER", nullable: false) .Annotation("Sqlite:Autoincrement", true), - DetailResourceId = table.Column(type: "TEXT", nullable: false), + ResourceId = table.Column(type: "TEXT", nullable: false), EventId = table.Column(type: "INTEGER", nullable: false), DetailTypeId = table.Column(type: "INTEGER", nullable: false), IntensityId = table.Column(type: "INTEGER", nullable: false), @@ -112,7 +112,7 @@ protected override void Up(MigrationBuilder migrationBuilder) }, constraints: table => { - table.PrimaryKey("PK_Details", x => x.DetailId); + table.PrimaryKey("PK_Details", x => x.Id); table.ForeignKey( name: "FK_Details_DetailTypes_DetailTypeId", column: x => x.DetailTypeId, @@ -123,13 +123,13 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "FK_Details_Events_EventId", column: x => x.EventId, principalTable: "Events", - principalColumn: "EventId", + principalColumn: "Id", onDelete: ReferentialAction.Cascade); table.ForeignKey( name: "FK_Details_Intensities_IntensityId", column: x => x.IntensityId, principalTable: "Intensities", - principalColumn: "IntensityId", + principalColumn: "Id", onDelete: ReferentialAction.Cascade); }); @@ -149,9 +149,9 @@ protected override void Up(MigrationBuilder migrationBuilder) column: "IntensityId"); migrationBuilder.CreateIndex( - name: "IX_Events_TypeEventTypeId", + name: "IX_Events_TypeId", table: "Events", - column: "TypeEventTypeId"); + column: "TypeId"); migrationBuilder.CreateIndex( name: "IX_Intensities_DetailTypeId", diff --git a/EventJournal.Data/Migrations/DatabaseContextModelSnapshot.cs b/EventJournal.Data/Migrations/DatabaseContextModelSnapshot.cs index 8736a5d..be0a3d8 100644 --- a/EventJournal.Data/Migrations/DatabaseContextModelSnapshot.cs +++ b/EventJournal.Data/Migrations/DatabaseContextModelSnapshot.cs @@ -19,16 +19,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("EventJournal.Data.Entities.Detail", b => { - b.Property("DetailId") + b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); b.Property("CreatedDate") .HasColumnType("TEXT"); - b.Property("DetailResourceId") - .HasColumnType("TEXT"); - b.Property("DetailTypeId") .HasColumnType("INTEGER"); @@ -42,10 +39,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(512) .HasColumnType("TEXT"); + b.Property("ResourceId") + .HasColumnType("TEXT"); + b.Property("UpdatedDate") .HasColumnType("TEXT"); - b.HasKey("DetailId"); + b.HasKey("Id"); b.HasIndex("DetailTypeId"); @@ -58,7 +58,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("EventJournal.Data.Entities.Event", b => { - b.Property("EventId") + b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); @@ -72,21 +72,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("EndTime") .HasColumnType("TEXT"); - b.Property("EventResourceId") + b.Property("ResourceId") .HasColumnType("TEXT"); b.Property("StartTime") .HasColumnType("TEXT"); - b.Property("TypeEventTypeId") + b.Property("TypeId") .HasColumnType("INTEGER"); b.Property("UpdatedDate") .HasColumnType("TEXT"); - b.HasKey("EventId"); + b.HasKey("Id"); - b.HasIndex("TypeEventTypeId"); + b.HasIndex("TypeId"); b.ToTable("Events"); }); @@ -121,7 +121,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.EventType", b => { - b.Property("EventTypeId") + b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); @@ -132,24 +132,24 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(500) .HasColumnType("TEXT"); - b.Property("EventTypeResourceId") - .HasColumnType("TEXT"); - b.Property("Name") .IsRequired() .HasColumnType("TEXT"); + b.Property("ResourceId") + .HasColumnType("TEXT"); + b.Property("UpdatedDate") .HasColumnType("TEXT"); - b.HasKey("EventTypeId"); + b.HasKey("Id"); b.ToTable("EventTypes"); }); modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.Intensity", b => { - b.Property("IntensityId") + b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); @@ -166,9 +166,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("DetailTypeId") .HasColumnType("INTEGER"); - b.Property("IntensityResourceId") - .HasColumnType("TEXT"); - b.Property("Level") .HasColumnType("INTEGER"); @@ -177,10 +174,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(50) .HasColumnType("TEXT"); + b.Property("ResourceId") + .HasColumnType("TEXT"); + b.Property("UpdatedDate") .HasColumnType("TEXT"); - b.HasKey("IntensityId"); + b.HasKey("Id"); b.HasIndex("DetailTypeId"); @@ -218,7 +218,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.HasOne("EventJournal.Data.Entities.UserTypes.EventType", "Type") .WithMany() - .HasForeignKey("TypeEventTypeId") + .HasForeignKey("TypeId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -228,7 +228,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.Intensity", b => { b.HasOne("EventJournal.Data.Entities.UserTypes.DetailType", null) - .WithMany("Intensities") + .WithMany("AllowedIntensities") .HasForeignKey("DetailTypeId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -241,7 +241,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.DetailType", b => { - b.Navigation("Intensities"); + b.Navigation("AllowedIntensities"); }); #pragma warning restore 612, 618 } From e710139ecf63e56a5b279eec80052598cf6212ee Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Mon, 18 Aug 2025 01:52:48 -0400 Subject: [PATCH 20/34] pdates to entities, attempt to add test data via cli (wip), mappers appear to work! --- EventJournal.CLI/Program.cs | 208 +++++++++--------- EventJournal.DomainModels/BaseDto.cs | 11 - EventJournal.DomainModels/DetailDto.cs | 9 +- EventJournal.DomainModels/DtoHelpers.cs | 43 ++++ EventJournal.DomainModels/EventDto.cs | 5 +- .../UserTypes/DetailTypeDto.cs | 8 +- .../UserTypes/EventTypeDto.cs | 5 +- .../UserTypes/IntensityDto.cs | 10 +- .../DomainMapperProfile.cs | 2 + EventJournal.DomainService/EventService.cs | 9 +- EventJournal.DomainService/IEventService.cs | 4 +- .../IUserTypeService.cs | 6 +- EventJournal.DomainService/UserTypeService.cs | 14 +- README.md | 18 +- 14 files changed, 193 insertions(+), 159 deletions(-) create mode 100644 EventJournal.DomainModels/DtoHelpers.cs diff --git a/EventJournal.CLI/Program.cs b/EventJournal.CLI/Program.cs index ebf43f3..f146c71 100644 --- a/EventJournal.CLI/Program.cs +++ b/EventJournal.CLI/Program.cs @@ -1,71 +1,74 @@ // See https://aka.ms/new-console-template for more information +using EventJournal.CLI; using EventJournal.Data; +using EventJournal.Data.UserTypeRepositories; +using EventJournal.DomainDto; using EventJournal.DomainService; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; - internal class Program { private static async Task Main(string[] args) { var services = CreateServiceCollection(); - var EventService = services.GetService() ?? throw new Exception("Unable to locate a valid Product Logic module"); - var UserTypeServes = services.GetService() ?? throw new Exception("Unable to locate a valid Order Logic module"); + var eventService = services.GetService() ?? throw new Exception("Unable to locate a valid Product Logic module"); + var userTypeService = services.GetService() ?? throw new Exception("Unable to locate a valid Order Logic module"); bool userIsDone = false; while (!userIsDone) { - //Console.WriteLine("Type '1' to add/update a product."); - //Console.WriteLine("Type '2' to view a product."); - //Console.WriteLine("Type '3' to view products that are in stock."); - //Console.WriteLine("Type '4' to view all products."); - - //Console.WriteLine("Type '6' to add/update an order."); - //Console.WriteLine("Type '7' to view an order."); - //Console.WriteLine("Type '8' to view all orders."); - //Console.WriteLine("Type 'a' to add some test data."); - //Console.WriteLine("Type 'x' to delete all data."); - //Console.WriteLine("Type 'q' to quit."); - - //// application will block here waiting for user to press - //var userInput = CLIUtilities.GetStringFromUser("===> ").ToLower() ?? ""; - - //switch (userInput[0]) { - // case 'q': - // userIsDone = true; - // break; - // case '1': - // await AddUpdateEntity(GetEntityFromUser()).ConfigureAwait(false); - // break; - // case '2': - // await ViewProduct().ConfigureAwait(false); - // break; - // case '3': - // await ViewInStockProducts().ConfigureAwait(false); - // break; - // case '4': - // await ViewAllProduct().ConfigureAwait(false); - // break; - // case '5': - - // break; - // case '6': - // await AddUpdateEntity(GetEntityFromUser()).ConfigureAwait(false); - // break; - // case '7': - // await ViewOrder().ConfigureAwait(false); - // break; - // case '8': - // await ViewallOrders().ConfigureAwait(false); - // break; - // case '9': - - // break; - // case 'a': - // await AddTestData(ProductService, OrderService).ConfigureAwait(false); - // break; - // case 'x': - // await DeleteAllData(ProductService, OrderService).ConfigureAwait(false); - // break; - //} + //Console.WriteLine("Type '1' to "); + //Console.WriteLine("Type '2' to "); + //Console.WriteLine("Type '3' to "); + //Console.WriteLine("Type '4' to "); + //Console.WriteLine("Type '6' to "); + //Console.WriteLine("Type '7' to "); + //Console.WriteLine("Type '8' to "); + + Console.WriteLine("Type '9' to view all data"); + Console.WriteLine("Type 'a' to add some test data."); + Console.WriteLine("Type 'x' to delete all data."); + Console.WriteLine("Type 'q' to quit."); + + // application will block here waiting for user to press + var userInput = CLIUtilities.GetStringFromUser("===> ").ToLower() ?? ""; + + switch (userInput[0]) { + case 'q': + userIsDone = true; + break; + // case '1': + // await AddUpdateEntity(GetEntityFromUser()).ConfigureAwait(false); + // break; + // case '2': + // await ViewProduct().ConfigureAwait(false); + // break; + // case '3': + // await ViewInStockProducts().ConfigureAwait(false); + // break; + // case '4': + // await ViewAllProduct().ConfigureAwait(false); + // break; + // case '5': + + // break; + // case '6': + // await AddUpdateEntity(GetEntityFromUser()).ConfigureAwait(false); + // break; + // case '7': + // await ViewOrder().ConfigureAwait(false); + // break; + case '8': + + break; + case '9': + await ViewallDataAsync().ConfigureAwait(false); + break; + case 'a': + await AddTestDataAsync(eventService, userTypeService).ConfigureAwait(false); + break; + case 'x': + await DeleteAllDataAsync(eventService, userTypeService).ConfigureAwait(false); + break; + } Console.WriteLine("\n===============================\n"); } //TODO: move to shared startup.cs and remove this method and remove microsoft.extension.hosting pkg @@ -74,8 +77,12 @@ static IServiceProvider CreateServiceCollection() { .AddDbContext(options => { options.UseSqlite($"Data Source={DatabaseContext.GetSqliteDbPath()}"); }) + .AddAutoMapper(cfg => { }, typeof(DomainMapperProfile)) .AddSingleton() .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddLogging(options => { @@ -86,28 +93,32 @@ static IServiceProvider CreateServiceCollection() { options.TimestampFormat = "HH:mm:ss.fff "; options.ColorBehavior = Microsoft.Extensions.Logging.Console.LoggerColorBehavior.Enabled; }); - }) - .AddAutoMapper(cfg => { }, typeof(DomainMapperProfile)); - + }); return servicecollection.BuildServiceProvider(); } - //async Task AddTestData(IProductService productService, IOrderService orderService) { - // Console.WriteLine("Adding/Resetting test data."); - // await AddUpdateEntity(new Product { ProductId = 1, Name = "Super Short Leash", Quantity = 10, Price = 1.99M }).ConfigureAwait(false); - // await AddUpdateEntity(new Product { ProductId = 2, Name = "Dry Cat Food", Quantity = 0, Price = 15.99M }).ConfigureAwait(false); - // await AddUpdateEntity(new Product { ProductId = 100, Name = "Designer Leash", Quantity = 1, Price = 99.99M }).ConfigureAwait(false); - // await AddUpdateEntity(new Order { OrderId = 1, OrderDate = DateTime.Now, OrderProducts = { new OrderProduct { ProductId = 100, OrderQuantity = 5, UnitPrice = 99.99M } } }).ConfigureAwait(false); - // await AddUpdateEntity(new Order { OrderId = 2, OrderDate = DateTime.Now, OrderProducts = { new OrderProduct { ProductId = 2, OrderQuantity = 3, UnitPrice = 15.99M } } }).ConfigureAwait(false); - //} - //static async Task DeleteAllData(IProductService productService, IOrderService orderService) { - // var products = await productService.GetProductsAsync().ConfigureAwait(false); - // products.ForEach(async p => await productService.RemoveProductAsync(p)); - - // var orders = await orderService.GetOrdersAsync().ConfigureAwait(false); - // orders.ForEach(async o => await orderService.RemoveOrderAsync(o).ConfigureAwait(false)); - //} - + async Task AddTestDataAsync(IEventService eventService, IUserTypeService userTypeService) { + Console.WriteLine("Adding/Resetting test data."); + await AddUpdateDtoAsync(EventDto.CreateDefaultEventDto()).ConfigureAwait(false); + //await AddUpdateEntity(new Product { ProductId = 2, Name = "Dry Cat Food", Quantity = 0, Price = 15.99M }).ConfigureAwait(false); + //await AddUpdateEntity(new Product { ProductId = 100, Name = "Designer Leash", Quantity = 1, Price = 99.99M }).ConfigureAwait(false); + //await AddUpdateEntity(new Order { OrderId = 1, OrderDate = DateTime.Now, OrderProducts = { new OrderProduct { ProductId = 100, OrderQuantity = 5, UnitPrice = 99.99M } } }).ConfigureAwait(false); + //await AddUpdateEntity(new Order { OrderId = 2, OrderDate = DateTime.Now, OrderProducts = { new OrderProduct { ProductId = 2, OrderQuantity = 3, UnitPrice = 15.99M } } }).ConfigureAwait(false); + } + static async Task DeleteAllDataAsync(IEventService eventService, IUserTypeService userTypeService) { + var events = await eventService.GetAllEventsAsync().ConfigureAwait(false); + foreach (var e in events) { + await eventService.DeleteEventAsync(e.EventResourceId).ConfigureAwait(false); + } + //var orders =( await userTypeService.GetOrdersAsync().ConfigureAwait(false)).ToList(); + //orders.ForEach(async o => await orderService.RemoveOrderAsync(o).ConfigureAwait(false)); + } + async Task ViewallDataAsync() { + var events = await eventService.GetAllEventsAsync().ConfigureAwait(false); + foreach (var e in events) { + Console.WriteLine(e.Serialize()); + } + } //static T? GetEntityFromUser() where T : EntityBase { // var json = CLIUtilities.GetStringFromUser($"Enter {typeof(T)} JSON: "); // T? entity = json.Deserialize(); @@ -185,44 +196,21 @@ static IServiceProvider CreateServiceCollection() { // products.ForEach(p => Console.WriteLine(p.Serialize())); //} - //async Task ViewallOrders() { - // var orders = await OrderService.GetOrdersAsync().ConfigureAwait(false); - // orders.ForEach(o => Console.WriteLine(o.ToString())); - //} - - //async Task AddUpdateEntity(T? entityUpdate) where T : EntityBase { + async Task AddUpdateDtoAsync(T? dtoUpdate) where T : BaseDto { - // if (entityUpdate == null) { - // Console.WriteLine("Nothing updated."); - // return; - // } + if (dtoUpdate == null) { + Console.WriteLine("Nothing updated."); + return; + } - // if (!ValidateEntity(entityUpdate)) { - // return; - // } + //if (!ValidateEntity(entityUpdate)) { + // return; + //} - // if (entityUpdate is Product product) { - // if (!await ProductService.ProductExists(product.ProductId)) { - // Console.WriteLine($"Product not found. Adding."); - // await AddEntity(product); - // return; - // } - // Console.WriteLine($"Updating product Id {product.ProductId}"); - // await ProductService.UpdateProduct(product).ConfigureAwait(false); - // return; - // } - // if (entityUpdate is Order order) { - // //TODO: edit PS10 instructions to include valid json now that OrderProduct is a thing. - // if (!await OrderService.OrderExists(order.OrderId)) { - // Console.WriteLine($"Order not found. Adding."); - // await AddEntity(order); - // return; - // } - // Console.WriteLine($"Updating order Id {order.OrderId}"); - // await OrderService.UpdateOrder(order).ConfigureAwait(false); - // return; - // } - //} + if (dtoUpdate is EventDto @event) { + await eventService.AddUpdateEventAsync(@event); + } + } } } \ No newline at end of file diff --git a/EventJournal.DomainModels/BaseDto.cs b/EventJournal.DomainModels/BaseDto.cs index 6151ef1..d0967db 100644 --- a/EventJournal.DomainModels/BaseDto.cs +++ b/EventJournal.DomainModels/BaseDto.cs @@ -14,16 +14,5 @@ public abstract class BaseDto { /// /// internal abstract void CopyUserValues(T source); - - } - - //TODO: is this needed? - public static partial class DtoHelpers { - public static T UpdateEntity(this T destination, T source) where T : BaseDto { - destination.CopyUserValues(source); - destination.CreatedDate = source.CreatedDate; - destination.UpdatedDate = DateTime.UtcNow; - return destination; - } } } \ No newline at end of file diff --git a/EventJournal.DomainModels/DetailDto.cs b/EventJournal.DomainModels/DetailDto.cs index 4befb9f..8d8f66a 100644 --- a/EventJournal.DomainModels/DetailDto.cs +++ b/EventJournal.DomainModels/DetailDto.cs @@ -5,9 +5,6 @@ namespace EventJournal.DomainDto { public class DetailDto : BaseDto { public Guid DetailResourceId { get { return ResourceId; } set { ResourceId = value; } } - [Required] - public virtual required EventDto Event { get; set; } - [Required] public virtual required DetailTypeDto DetailType { get; set; } @@ -17,9 +14,13 @@ public class DetailDto : BaseDto { [MaxLength(512)] public string? Notes { get; set; } + public static DetailDto CreateDefaultDetailDto() { + var detailType = DetailTypeDto.CreateDefaultDetailTypeDto(); + return new DetailDto { DetailResourceId = Guid.NewGuid(), DetailType = detailType, Intensity = IntensityDto.CreateDefaultIntensityDto(detailType.ResourceId), Notes = "Notes\nmore notes" }; + } + internal override void CopyUserValues(T source) { var sourceDetail = source as DetailDto ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(DetailDto)}"); - Event = sourceDetail.Event; DetailType = sourceDetail.DetailType; Intensity = sourceDetail.Intensity; Notes = sourceDetail.Notes; diff --git a/EventJournal.DomainModels/DtoHelpers.cs b/EventJournal.DomainModels/DtoHelpers.cs new file mode 100644 index 0000000..c6d38a7 --- /dev/null +++ b/EventJournal.DomainModels/DtoHelpers.cs @@ -0,0 +1,43 @@ +using EventJournal.DomainDto.UserTypes; +using System.Diagnostics; +using System.Text.Json; + +namespace EventJournal.DomainDto { + public static partial class DtoHelpers { + public static T UpdateEntity(this T destination, T source) where T : BaseDto { + destination.CopyUserValues(source); + destination.CreatedDate = source.CreatedDate; + destination.UpdatedDate = DateTime.UtcNow; + return destination; + } + + public static string Serialize(this T entity) where T : BaseDto { + //TODO: use reflection to find objects that inherit from BaseDto and their type + if (entity is DetailDto detail && detail != null) { + return JsonSerializer.Serialize(detail); + } + if (entity is EventDto @event && @event != null) { + return JsonSerializer.Serialize(@event); + } + if (entity is DetailTypeDto detailType && detailType != null) { + return JsonSerializer.Serialize(detailType); + } + if (entity is EventTypeDto eventTypeDto && eventTypeDto != null) { + return JsonSerializer.Serialize(eventTypeDto); + } + if (entity is IntensityDto intensityDto && intensityDto != null) { + return JsonSerializer.Serialize(intensityDto); + } + return JsonSerializer.Serialize(entity); + } + + public static T? Deserialize(this string json) where T : BaseDto { + //TODO: does this work properly for inherited types? + return JsonSerializer.Deserialize(json); + } + + public static string ToString(this T entity) where T : BaseDto { + return entity.Serialize(); + } + } +} diff --git a/EventJournal.DomainModels/EventDto.cs b/EventJournal.DomainModels/EventDto.cs index 0a09b95..e797113 100644 --- a/EventJournal.DomainModels/EventDto.cs +++ b/EventJournal.DomainModels/EventDto.cs @@ -6,7 +6,7 @@ public class EventDto : BaseDto { public Guid EventResourceId { get { return ResourceId; } set { ResourceId = value; } } [Required] - public EventTypeDto Type { get; set; } = EventTypeDto.CreateDefaultEventType(); + public EventTypeDto Type { get; set; } = EventTypeDto.CreateDefaultEventTypeDto(); [Required] public DateTime StartTime { get; set; } = DateTime.Now; @@ -17,6 +17,9 @@ public class EventDto : BaseDto { public IEnumerable Details { get; set; } = []; + public static EventDto CreateDefaultEventDto() { + return new EventDto { EventResourceId = Guid.NewGuid(), StartTime = DateTime.Now, EndTime = DateTime.Now.AddMinutes(1).AddSeconds(1), Description = "Description", Details = [ DetailDto.CreateDefaultDetailDto()] }; + } internal override void CopyUserValues(T source) { var soruceEvent = source as EventDto ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(EventDto)}"); Type = soruceEvent.Type; diff --git a/EventJournal.DomainModels/UserTypes/DetailTypeDto.cs b/EventJournal.DomainModels/UserTypes/DetailTypeDto.cs index ef7a7c1..c9fcd1a 100644 --- a/EventJournal.DomainModels/UserTypes/DetailTypeDto.cs +++ b/EventJournal.DomainModels/UserTypes/DetailTypeDto.cs @@ -10,7 +10,13 @@ public class DetailTypeDto : BaseDto { [MaxLength(500)] public string? Description { get; set; } - public IEnumerable Intensities { get; set; } = []; + //TODO: is this useful/needed? + public IEnumerable AllowedIntensities { get; set; } = []; + + //TODO: does this belong here?? + public static DetailTypeDto CreateDefaultDetailTypeDto() { + return new DetailTypeDto { DetailTypeResourceId = Guid.NewGuid(), Description = "Description", Name = "Default DTO Type" }; + } internal override void CopyUserValues(T source) { var sourceDetailType = source as DetailTypeDto ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(DetailTypeDto)}"); diff --git a/EventJournal.DomainModels/UserTypes/EventTypeDto.cs b/EventJournal.DomainModels/UserTypes/EventTypeDto.cs index 7727598..9cf350b 100644 --- a/EventJournal.DomainModels/UserTypes/EventTypeDto.cs +++ b/EventJournal.DomainModels/UserTypes/EventTypeDto.cs @@ -13,7 +13,7 @@ public class EventTypeDto : BaseDto { //TODO: does this belong here?? public static IEnumerable DefaultEventTypes() { return [ - CreateDefaultEventType(), + CreateDefaultEventTypeDto(), new EventTypeDto { EventTypeResourceId = Guid.NewGuid(), Name = "Exercise" }, new EventTypeDto { EventTypeResourceId = Guid.NewGuid(), Name = "Bathroom Visit" }, new EventTypeDto { EventTypeResourceId = Guid.NewGuid(), Name = "Food Consumption" }, @@ -22,7 +22,7 @@ public static IEnumerable DefaultEventTypes() { } //TODO: does this belong here?? - public static EventTypeDto CreateDefaultEventType() { + public static EventTypeDto CreateDefaultEventTypeDto() { return new EventTypeDto { EventTypeResourceId = Guid.NewGuid(), Name = "Random Event", Description = "Use for tracking random things like onset of pain, headache, or whatever that isn't directly associated with a specific even type." }; } @@ -32,6 +32,5 @@ internal override void CopyUserValues(T source) { Description = sourceEventType.Description; } } - } diff --git a/EventJournal.DomainModels/UserTypes/IntensityDto.cs b/EventJournal.DomainModels/UserTypes/IntensityDto.cs index cb06daf..24721d8 100644 --- a/EventJournal.DomainModels/UserTypes/IntensityDto.cs +++ b/EventJournal.DomainModels/UserTypes/IntensityDto.cs @@ -1,6 +1,5 @@ using EventJournal.DomainDto.Enumerations; using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; namespace EventJournal.DomainDto.UserTypes { public class IntensityDto : BaseDto { @@ -18,9 +17,13 @@ public class IntensityDto : BaseDto { [Required] public required SortType DefaultSortType { get; set; } - [ForeignKey(nameof(DetailTypeId))] + [Required] - public required int DetailTypeId { get; set; } + public required Guid DetailTypeId { get; set; } + //TODO: does this belong here?? + public static IntensityDto CreateDefaultIntensityDto(Guid detailTypeId) { + return new IntensityDto { IntensityResourceId = Guid.NewGuid(), DefaultSortType = SortType.Descending, Level = 1, Name = "Default Intensity DTO", Description = "Description", DetailTypeId=detailTypeId }; + } internal override void CopyUserValues(T source) { var sourceIntensity = source as IntensityDto ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(IntensityDto)}"); @@ -28,7 +31,6 @@ internal override void CopyUserValues(T source) { Level = sourceIntensity.Level; Description = sourceIntensity.Description; DefaultSortType = sourceIntensity.DefaultSortType; - DetailTypeId = sourceIntensity.DetailTypeId; } } } diff --git a/EventJournal.DomainService/DomainMapperProfile.cs b/EventJournal.DomainService/DomainMapperProfile.cs index 8337903..26a6a6c 100644 --- a/EventJournal.DomainService/DomainMapperProfile.cs +++ b/EventJournal.DomainService/DomainMapperProfile.cs @@ -7,6 +7,8 @@ namespace EventJournal.DomainService { public class DomainMapperProfile : Profile { public DomainMapperProfile() { + + //TODO: use reflection to find objects that inherit from BaseDto and map them CreateMap().ReverseMap(); CreateMap().ReverseMap(); CreateMap().ReverseMap(); diff --git a/EventJournal.DomainService/EventService.cs b/EventJournal.DomainService/EventService.cs index fe7f679..ddf5828 100644 --- a/EventJournal.DomainService/EventService.cs +++ b/EventJournal.DomainService/EventService.cs @@ -3,6 +3,7 @@ using EventJournal.Data.Entities; using EventJournal.DomainDto; using EventJournal.DomainService.Exceptions; +using System.Linq; namespace EventJournal.DomainService { public class EventService( @@ -10,8 +11,8 @@ public class EventService( IDetailRepository detailRepository, IMapper mapper) : IEventService { - public async Task> GetAllEventsAsync() { - return mapper.Map>(await eventRepository.GetAllAsync().ConfigureAwait(false)); + public async Task> GetAllEventsAsync() { + return mapper.Map>(await eventRepository.GetAllAsync().ConfigureAwait(false)); } public async Task GetEventByIdAsync(Guid resourceId) { return mapper.Map(await eventRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false)); @@ -27,8 +28,8 @@ public async Task DeleteEventAsync(Guid resourceId) { await eventRepository.DeleteAsync(entity).ConfigureAwait(false); } - public async Task> GetAllDetailsAsync() { - return mapper.Map>(await detailRepository.GetAllAsync().ConfigureAwait(false)); + public async Task> GetAllDetailsAsync() { + return mapper.Map>(await detailRepository.GetAllAsync().ConfigureAwait(false)); } public async Task GetDetailByIdAsync(Guid resourceId) { return mapper.Map(await detailRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false)); diff --git a/EventJournal.DomainService/IEventService.cs b/EventJournal.DomainService/IEventService.cs index dee664e..0e77d0d 100644 --- a/EventJournal.DomainService/IEventService.cs +++ b/EventJournal.DomainService/IEventService.cs @@ -2,12 +2,12 @@ namespace EventJournal.DomainService { public interface IEventService { - Task> GetAllEventsAsync(); + Task> GetAllEventsAsync(); Task GetEventByIdAsync(Guid resourceId); Task AddUpdateEventAsync(EventDto dto); Task DeleteEventAsync(Guid resourceId); - Task> GetAllDetailsAsync(); + Task> GetAllDetailsAsync(); Task GetDetailByIdAsync(Guid resourceId); Task AddUpdateDetailAsync(DetailDto dto); Task DeleteDetailAsync(Guid resourceId); diff --git a/EventJournal.DomainService/IUserTypeService.cs b/EventJournal.DomainService/IUserTypeService.cs index 77e4159..af4bed1 100644 --- a/EventJournal.DomainService/IUserTypeService.cs +++ b/EventJournal.DomainService/IUserTypeService.cs @@ -4,17 +4,17 @@ namespace EventJournal.DomainService { public interface IUserTypeService { - Task> GetAllDetailTypesAsync(); + Task> GetAllDetailTypesAsync(); Task GetDetailTypeByIdAsync(Guid resourceId); Task AddUpdateDetailTypeAsync(DetailTypeDto dto); Task DeleteDetailTypeAsync(Guid resourceId); - Task> GetAllEventTypesAsync(); + Task> GetAllEventTypesAsync(); Task GetEventTypeByIdAsync(Guid resourceId); Task AddUpdateEventTypeAsync(EventTypeDto dto); Task DeleteEventTypeAsync(Guid resourceId); - Task> GetAllIntensitiesAsync(); + Task> GetAllIntensitiesAsync(); Task GetIntensityByIdAsync(Guid resourceId); Task AddUpdateIntensityAsync(IntensityDto dto); Task DeleteIntensityAsync(Guid resourceId); diff --git a/EventJournal.DomainService/UserTypeService.cs b/EventJournal.DomainService/UserTypeService.cs index 7126791..51d3ab3 100644 --- a/EventJournal.DomainService/UserTypeService.cs +++ b/EventJournal.DomainService/UserTypeService.cs @@ -9,10 +9,10 @@ public class UserTypeService( IDetailTypeRepository detailRepository, IEventTypeRepository eventTypeRepository, IIntensityRepository intensityRepository, - Mapper mapper) : IUserTypeService { + IMapper mapper) : IUserTypeService { - public async Task> GetAllDetailTypesAsync() { - return mapper.Map>(await detailRepository.GetAllAsync().ConfigureAwait(false)); + public async Task> GetAllDetailTypesAsync() { + return mapper.Map>(await detailRepository.GetAllAsync().ConfigureAwait(false)); } public async Task GetDetailTypeByIdAsync(Guid resourceId) { return mapper.Map(await detailRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false)); @@ -28,8 +28,8 @@ public async Task DeleteDetailTypeAsync(Guid resourceId) { await detailRepository.DeleteAsync(entity); } - public async Task> GetAllEventTypesAsync() { - return mapper.Map>(await eventTypeRepository.GetAllAsync().ConfigureAwait(false)); + public async Task> GetAllEventTypesAsync() { + return mapper.Map>(await eventTypeRepository.GetAllAsync().ConfigureAwait(false)); } public async Task GetEventTypeByIdAsync(Guid resourceId) { return mapper.Map(await eventTypeRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false)); @@ -45,8 +45,8 @@ public async Task DeleteEventTypeAsync(Guid resourceId) { await eventTypeRepository.DeleteAsync(entity); } - public async Task> GetAllIntensitiesAsync() { - return mapper.Map>(await intensityRepository.GetAllAsync().ConfigureAwait(false)); + public async Task> GetAllIntensitiesAsync() { + return mapper.Map>(await intensityRepository.GetAllAsync().ConfigureAwait(false)); } public async Task GetIntensityByIdAsync(Guid resourceId) { return mapper.Map(await intensityRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false)); diff --git a/README.md b/README.md index 19d794d..f7fb53c 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,9 @@ The concept of Intensity is still a bit muddy, but the idea is that events of a ## Features -Features include: -User definable event types (ex. exercise, meal, headache ) -USer definable detail types (ex. muscle pain, bleeding, restaurant or food eaten, pain location, pain level, nausea ) -User definable detail intensity descriptions (tied to numeric values for graphing/data presentation purposes) +- User definable event types (ex. exercise, meal, headache ) +- USer definable detail types (ex. muscle pain, bleeding, restaurant or food eaten, pain location, pain level, nausea ) +- User definable detail intensity descriptions (tied to numeric values for graphing/data presentation purposes) ## NOTES @@ -27,7 +26,7 @@ Events are the central entity. An `Event` has an `EventType` and one or more `De - Slight to mild Discomfort (1) - I need an OTC painkiller (2) - I'm going to limit my activity (3) -- I can't function (4) +- I can't function normally (4) - Take me to the ER (5) #### Bleeding could be @@ -48,11 +47,12 @@ Events are the central entity. An `Event` has an `EventType` and one or more `De #### Entity Framework help EF https://learn.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs=netcore-cli + +### NEXT STEPS +- add default data from CLI isn't working + - looks like you have to add detail type first, then add intensities + - then you can add everything else? ### TODO -- !!!! fix entity framework - - Entities moved name spaces so we likely have to remove migrations and start all over - - need to finish models first -- refactor services to consume DTOs (models) instead of entities directly. - unit tests for repositories and services. Also base entity code? - CLI interface to call service methods - Web API to call service methods From e6ceb5ef1dda723e0f9265867978bb7a256ccc73 Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Tue, 19 Aug 2025 01:39:20 -0400 Subject: [PATCH 21/34] updates to entities, and dtos, initial mappings should work --- EventJournal.CLI/Program.cs | 13 +- EventJournal.Data/Entities/BaseEntity.cs | 11 +- EventJournal.Data/Entities/Detail.cs | 6 +- EventJournal.Data/Entities/EntityHelper.cs | 10 + EventJournal.Data/Entities/Event.cs | 4 +- .../Entities/UserTypes/EventType.cs | 5 - ...0250818052438_newInitialCreate.Designer.cs | 252 ------------------ .../20250818052438_newInitialCreate.cs | 181 ------------- .../DatabaseContextModelSnapshot.cs | 249 ----------------- EventJournal.DomainModels/DetailDto.cs | 11 +- .../{DtoHelpers.cs => DtoHelper.cs} | 3 +- EventJournal.DomainModels/EventDto.cs | 15 +- .../UserTypes/DetailTypeDto.cs | 8 +- .../UserTypes/EventTypeDto.cs | 20 +- .../UserTypes/IntensityDto.cs | 11 +- EventJournal.DomainService/EventService.cs | 5 +- 16 files changed, 74 insertions(+), 730 deletions(-) create mode 100644 EventJournal.Data/Entities/EntityHelper.cs delete mode 100644 EventJournal.Data/Migrations/20250818052438_newInitialCreate.Designer.cs delete mode 100644 EventJournal.Data/Migrations/20250818052438_newInitialCreate.cs delete mode 100644 EventJournal.Data/Migrations/DatabaseContextModelSnapshot.cs rename EventJournal.DomainModels/{DtoHelpers.cs => DtoHelper.cs} (96%) diff --git a/EventJournal.CLI/Program.cs b/EventJournal.CLI/Program.cs index f146c71..69efc14 100644 --- a/EventJournal.CLI/Program.cs +++ b/EventJournal.CLI/Program.cs @@ -23,7 +23,7 @@ private static async Task Main(string[] args) { //Console.WriteLine("Type '7' to "); //Console.WriteLine("Type '8' to "); - Console.WriteLine("Type '9' to view all data"); + Console.WriteLine("Type 'v' to view all data"); Console.WriteLine("Type 'a' to add some test data."); Console.WriteLine("Type 'x' to delete all data."); Console.WriteLine("Type 'q' to quit."); @@ -56,10 +56,11 @@ private static async Task Main(string[] args) { // case '7': // await ViewOrder().ConfigureAwait(false); // break; - case '8': - - break; - case '9': + //case '8': + // break; + //case '9': + // break; + case 'v': await ViewallDataAsync().ConfigureAwait(false); break; case 'a': @@ -99,7 +100,7 @@ static IServiceProvider CreateServiceCollection() { } async Task AddTestDataAsync(IEventService eventService, IUserTypeService userTypeService) { Console.WriteLine("Adding/Resetting test data."); - await AddUpdateDtoAsync(EventDto.CreateDefaultEventDto()).ConfigureAwait(false); + await AddUpdateDtoAsync(EventDto.DefaultEventDto()).ConfigureAwait(false); //await AddUpdateEntity(new Product { ProductId = 2, Name = "Dry Cat Food", Quantity = 0, Price = 15.99M }).ConfigureAwait(false); //await AddUpdateEntity(new Product { ProductId = 100, Name = "Designer Leash", Quantity = 1, Price = 99.99M }).ConfigureAwait(false); //await AddUpdateEntity(new Order { OrderId = 1, OrderDate = DateTime.Now, OrderProducts = { new OrderProduct { ProductId = 100, OrderQuantity = 5, UnitPrice = 99.99M } } }).ConfigureAwait(false); diff --git a/EventJournal.Data/Entities/BaseEntity.cs b/EventJournal.Data/Entities/BaseEntity.cs index a53b9ad..7fc4e6a 100644 --- a/EventJournal.Data/Entities/BaseEntity.cs +++ b/EventJournal.Data/Entities/BaseEntity.cs @@ -19,19 +19,10 @@ public abstract class BaseEntity { /// /// This method is predominantly for updating an entity based on the values in another entity. - /// Copy user values (ids and audit info (created, last modified, etc) should NOT be copied! + /// Ids and audit info (created, last modified, etc) should NOT be copied! /// /// /// internal abstract void CopyUserValues(T source); } - - public static partial class EntityHelpers { - public static T UpdateEntity(this T destination, T source) where T : BaseEntity { - destination.CopyUserValues(source); - destination.CreatedDate = source.CreatedDate; - destination.UpdatedDate = DateTime.UtcNow; - return destination; - } - } } diff --git a/EventJournal.Data/Entities/Detail.cs b/EventJournal.Data/Entities/Detail.cs index 7ed0901..39b4a6a 100644 --- a/EventJournal.Data/Entities/Detail.cs +++ b/EventJournal.Data/Entities/Detail.cs @@ -10,13 +10,13 @@ public class Detail : BaseEntity { public new Guid ResourceId { get { return base.ResourceId; } set { base.ResourceId = value; } } [Required] - public virtual required Event Event { get; set; } + public required Event Event { get; set; } [Required] - public virtual required DetailType DetailType { get; set; } + public required DetailType DetailType { get; set; } [Required] - public virtual required Intensity Intensity { get; set; } + public required Intensity Intensity { get; set; } [MaxLength(512)] public string? Notes { get; set; } diff --git a/EventJournal.Data/Entities/EntityHelper.cs b/EventJournal.Data/Entities/EntityHelper.cs new file mode 100644 index 0000000..55fb593 --- /dev/null +++ b/EventJournal.Data/Entities/EntityHelper.cs @@ -0,0 +1,10 @@ +namespace EventJournal.Data.Entities { + public static class EntityHelper { + public static T UpdateEntity(this T destination, T source) where T : BaseEntity { + destination.CopyUserValues(source); + destination.CreatedDate = source.CreatedDate; + destination.UpdatedDate = DateTime.UtcNow; + return destination; + } + } +} diff --git a/EventJournal.Data/Entities/Event.cs b/EventJournal.Data/Entities/Event.cs index 49e4a4e..6a0ed98 100644 --- a/EventJournal.Data/Entities/Event.cs +++ b/EventJournal.Data/Entities/Event.cs @@ -10,7 +10,7 @@ public class Event : BaseEntity { public new Guid ResourceId { get { return base.ResourceId; } set { base.ResourceId = value; } } [Required] - public EventType Type { get; set; } = EventType.CreateDefaultEventType(); + public required EventType EventType { get; set; } [Required] public DateTime StartTime { get; set; } = DateTime.Now; @@ -23,7 +23,7 @@ public class Event : BaseEntity { internal override void CopyUserValues(T source) { var soruceEvent = source as Event ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(Event)}"); - Type = soruceEvent.Type; + EventType = soruceEvent.EventType; StartTime = soruceEvent.StartTime; EndTime = soruceEvent.EndTime; Description = soruceEvent.Description; diff --git a/EventJournal.Data/Entities/UserTypes/EventType.cs b/EventJournal.Data/Entities/UserTypes/EventType.cs index 49e5efc..40f103e 100644 --- a/EventJournal.Data/Entities/UserTypes/EventType.cs +++ b/EventJournal.Data/Entities/UserTypes/EventType.cs @@ -14,11 +14,6 @@ public class EventType : BaseEntity { [MaxLength(500)] public string? Description { get; set; } - //TODO: does this belong here?? - public static EventType CreateDefaultEventType() { - return new EventType { ResourceId = Guid.NewGuid(), Name = "Random Event", Description = "Use for tracking random things like onset of pain, headache, or whatever that isn't directly associated with a specific even type." }; - } - internal override void CopyUserValues(T source) { var sourceEventType = source as EventType ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(EventType)}"); Name = sourceEventType.Name; diff --git a/EventJournal.Data/Migrations/20250818052438_newInitialCreate.Designer.cs b/EventJournal.Data/Migrations/20250818052438_newInitialCreate.Designer.cs deleted file mode 100644 index cbd9fc7..0000000 --- a/EventJournal.Data/Migrations/20250818052438_newInitialCreate.Designer.cs +++ /dev/null @@ -1,252 +0,0 @@ -// -using System; -using EventJournal.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace EventJournal.Data.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20250818052438_newInitialCreate")] - partial class newInitialCreate - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.8"); - - modelBuilder.Entity("EventJournal.Data.Entities.Detail", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedDate") - .HasColumnType("TEXT"); - - b.Property("DetailTypeId") - .HasColumnType("INTEGER"); - - b.Property("EventId") - .HasColumnType("INTEGER"); - - b.Property("IntensityId") - .HasColumnType("INTEGER"); - - b.Property("Notes") - .HasMaxLength(512) - .HasColumnType("TEXT"); - - b.Property("ResourceId") - .HasColumnType("TEXT"); - - b.Property("UpdatedDate") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("DetailTypeId"); - - b.HasIndex("EventId"); - - b.HasIndex("IntensityId"); - - b.ToTable("Details"); - }); - - modelBuilder.Entity("EventJournal.Data.Entities.Event", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedDate") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasMaxLength(500) - .HasColumnType("TEXT"); - - b.Property("EndTime") - .HasColumnType("TEXT"); - - b.Property("ResourceId") - .HasColumnType("TEXT"); - - b.Property("StartTime") - .HasColumnType("TEXT"); - - b.Property("TypeId") - .HasColumnType("INTEGER"); - - b.Property("UpdatedDate") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("TypeId"); - - b.ToTable("Events"); - }); - - modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.DetailType", b => - { - b.Property("DetailTypeId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedDate") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasMaxLength(500) - .HasColumnType("TEXT"); - - b.Property("DetailTypeResourceId") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("UpdatedDate") - .HasColumnType("TEXT"); - - b.HasKey("DetailTypeId"); - - b.ToTable("DetailTypes"); - }); - - modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.EventType", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedDate") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasMaxLength(500) - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("ResourceId") - .HasColumnType("TEXT"); - - b.Property("UpdatedDate") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("EventTypes"); - }); - - modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.Intensity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedDate") - .HasColumnType("TEXT"); - - b.Property("DefaultSortType") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasMaxLength(500) - .HasColumnType("TEXT"); - - b.Property("DetailTypeId") - .HasColumnType("INTEGER"); - - b.Property("Level") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("ResourceId") - .HasColumnType("TEXT"); - - b.Property("UpdatedDate") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("DetailTypeId"); - - b.ToTable("Intensities"); - }); - - modelBuilder.Entity("EventJournal.Data.Entities.Detail", b => - { - b.HasOne("EventJournal.Data.Entities.UserTypes.DetailType", "DetailType") - .WithMany() - .HasForeignKey("DetailTypeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("EventJournal.Data.Entities.Event", "Event") - .WithMany("Details") - .HasForeignKey("EventId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("EventJournal.Data.Entities.UserTypes.Intensity", "Intensity") - .WithMany() - .HasForeignKey("IntensityId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("DetailType"); - - b.Navigation("Event"); - - b.Navigation("Intensity"); - }); - - modelBuilder.Entity("EventJournal.Data.Entities.Event", b => - { - b.HasOne("EventJournal.Data.Entities.UserTypes.EventType", "Type") - .WithMany() - .HasForeignKey("TypeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Type"); - }); - - modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.Intensity", b => - { - b.HasOne("EventJournal.Data.Entities.UserTypes.DetailType", null) - .WithMany("AllowedIntensities") - .HasForeignKey("DetailTypeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("EventJournal.Data.Entities.Event", b => - { - b.Navigation("Details"); - }); - - modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.DetailType", b => - { - b.Navigation("AllowedIntensities"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/EventJournal.Data/Migrations/20250818052438_newInitialCreate.cs b/EventJournal.Data/Migrations/20250818052438_newInitialCreate.cs deleted file mode 100644 index eb23be1..0000000 --- a/EventJournal.Data/Migrations/20250818052438_newInitialCreate.cs +++ /dev/null @@ -1,181 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace EventJournal.Data.Migrations -{ - /// - public partial class newInitialCreate : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "DetailTypes", - columns: table => new - { - DetailTypeId = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - DetailTypeResourceId = table.Column(type: "TEXT", nullable: false), - Name = table.Column(type: "TEXT", nullable: false), - Description = table.Column(type: "TEXT", maxLength: 500, nullable: true), - CreatedDate = table.Column(type: "TEXT", nullable: false), - UpdatedDate = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_DetailTypes", x => x.DetailTypeId); - }); - - migrationBuilder.CreateTable( - name: "EventTypes", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - ResourceId = table.Column(type: "TEXT", nullable: false), - Name = table.Column(type: "TEXT", nullable: false), - Description = table.Column(type: "TEXT", maxLength: 500, nullable: true), - CreatedDate = table.Column(type: "TEXT", nullable: false), - UpdatedDate = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_EventTypes", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Intensities", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - ResourceId = table.Column(type: "TEXT", nullable: false), - Name = table.Column(type: "TEXT", maxLength: 50, nullable: false), - Level = table.Column(type: "INTEGER", nullable: false), - Description = table.Column(type: "TEXT", maxLength: 500, nullable: true), - DefaultSortType = table.Column(type: "INTEGER", nullable: false), - DetailTypeId = table.Column(type: "INTEGER", nullable: false), - CreatedDate = table.Column(type: "TEXT", nullable: false), - UpdatedDate = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Intensities", x => x.Id); - table.ForeignKey( - name: "FK_Intensities_DetailTypes_DetailTypeId", - column: x => x.DetailTypeId, - principalTable: "DetailTypes", - principalColumn: "DetailTypeId", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Events", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - ResourceId = table.Column(type: "TEXT", nullable: false), - TypeId = table.Column(type: "INTEGER", nullable: false), - StartTime = table.Column(type: "TEXT", nullable: false), - EndTime = table.Column(type: "TEXT", nullable: true), - Description = table.Column(type: "TEXT", maxLength: 500, nullable: true), - CreatedDate = table.Column(type: "TEXT", nullable: false), - UpdatedDate = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Events", x => x.Id); - table.ForeignKey( - name: "FK_Events_EventTypes_TypeId", - column: x => x.TypeId, - principalTable: "EventTypes", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Details", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - ResourceId = table.Column(type: "TEXT", nullable: false), - EventId = table.Column(type: "INTEGER", nullable: false), - DetailTypeId = table.Column(type: "INTEGER", nullable: false), - IntensityId = table.Column(type: "INTEGER", nullable: false), - Notes = table.Column(type: "TEXT", maxLength: 512, nullable: true), - CreatedDate = table.Column(type: "TEXT", nullable: false), - UpdatedDate = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Details", x => x.Id); - table.ForeignKey( - name: "FK_Details_DetailTypes_DetailTypeId", - column: x => x.DetailTypeId, - principalTable: "DetailTypes", - principalColumn: "DetailTypeId", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Details_Events_EventId", - column: x => x.EventId, - principalTable: "Events", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Details_Intensities_IntensityId", - column: x => x.IntensityId, - principalTable: "Intensities", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_Details_DetailTypeId", - table: "Details", - column: "DetailTypeId"); - - migrationBuilder.CreateIndex( - name: "IX_Details_EventId", - table: "Details", - column: "EventId"); - - migrationBuilder.CreateIndex( - name: "IX_Details_IntensityId", - table: "Details", - column: "IntensityId"); - - migrationBuilder.CreateIndex( - name: "IX_Events_TypeId", - table: "Events", - column: "TypeId"); - - migrationBuilder.CreateIndex( - name: "IX_Intensities_DetailTypeId", - table: "Intensities", - column: "DetailTypeId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Details"); - - migrationBuilder.DropTable( - name: "Events"); - - migrationBuilder.DropTable( - name: "Intensities"); - - migrationBuilder.DropTable( - name: "EventTypes"); - - migrationBuilder.DropTable( - name: "DetailTypes"); - } - } -} diff --git a/EventJournal.Data/Migrations/DatabaseContextModelSnapshot.cs b/EventJournal.Data/Migrations/DatabaseContextModelSnapshot.cs deleted file mode 100644 index be0a3d8..0000000 --- a/EventJournal.Data/Migrations/DatabaseContextModelSnapshot.cs +++ /dev/null @@ -1,249 +0,0 @@ -// -using System; -using EventJournal.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace EventJournal.Data.Migrations -{ - [DbContext(typeof(DatabaseContext))] - partial class DatabaseContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.8"); - - modelBuilder.Entity("EventJournal.Data.Entities.Detail", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedDate") - .HasColumnType("TEXT"); - - b.Property("DetailTypeId") - .HasColumnType("INTEGER"); - - b.Property("EventId") - .HasColumnType("INTEGER"); - - b.Property("IntensityId") - .HasColumnType("INTEGER"); - - b.Property("Notes") - .HasMaxLength(512) - .HasColumnType("TEXT"); - - b.Property("ResourceId") - .HasColumnType("TEXT"); - - b.Property("UpdatedDate") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("DetailTypeId"); - - b.HasIndex("EventId"); - - b.HasIndex("IntensityId"); - - b.ToTable("Details"); - }); - - modelBuilder.Entity("EventJournal.Data.Entities.Event", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedDate") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasMaxLength(500) - .HasColumnType("TEXT"); - - b.Property("EndTime") - .HasColumnType("TEXT"); - - b.Property("ResourceId") - .HasColumnType("TEXT"); - - b.Property("StartTime") - .HasColumnType("TEXT"); - - b.Property("TypeId") - .HasColumnType("INTEGER"); - - b.Property("UpdatedDate") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("TypeId"); - - b.ToTable("Events"); - }); - - modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.DetailType", b => - { - b.Property("DetailTypeId") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedDate") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasMaxLength(500) - .HasColumnType("TEXT"); - - b.Property("DetailTypeResourceId") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("UpdatedDate") - .HasColumnType("TEXT"); - - b.HasKey("DetailTypeId"); - - b.ToTable("DetailTypes"); - }); - - modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.EventType", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedDate") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasMaxLength(500) - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("ResourceId") - .HasColumnType("TEXT"); - - b.Property("UpdatedDate") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("EventTypes"); - }); - - modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.Intensity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedDate") - .HasColumnType("TEXT"); - - b.Property("DefaultSortType") - .HasColumnType("INTEGER"); - - b.Property("Description") - .HasMaxLength(500) - .HasColumnType("TEXT"); - - b.Property("DetailTypeId") - .HasColumnType("INTEGER"); - - b.Property("Level") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("ResourceId") - .HasColumnType("TEXT"); - - b.Property("UpdatedDate") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("DetailTypeId"); - - b.ToTable("Intensities"); - }); - - modelBuilder.Entity("EventJournal.Data.Entities.Detail", b => - { - b.HasOne("EventJournal.Data.Entities.UserTypes.DetailType", "DetailType") - .WithMany() - .HasForeignKey("DetailTypeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("EventJournal.Data.Entities.Event", "Event") - .WithMany("Details") - .HasForeignKey("EventId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("EventJournal.Data.Entities.UserTypes.Intensity", "Intensity") - .WithMany() - .HasForeignKey("IntensityId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("DetailType"); - - b.Navigation("Event"); - - b.Navigation("Intensity"); - }); - - modelBuilder.Entity("EventJournal.Data.Entities.Event", b => - { - b.HasOne("EventJournal.Data.Entities.UserTypes.EventType", "Type") - .WithMany() - .HasForeignKey("TypeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Type"); - }); - - modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.Intensity", b => - { - b.HasOne("EventJournal.Data.Entities.UserTypes.DetailType", null) - .WithMany("AllowedIntensities") - .HasForeignKey("DetailTypeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("EventJournal.Data.Entities.Event", b => - { - b.Navigation("Details"); - }); - - modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.DetailType", b => - { - b.Navigation("AllowedIntensities"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/EventJournal.DomainModels/DetailDto.cs b/EventJournal.DomainModels/DetailDto.cs index 8d8f66a..5425241 100644 --- a/EventJournal.DomainModels/DetailDto.cs +++ b/EventJournal.DomainModels/DetailDto.cs @@ -14,9 +14,14 @@ public class DetailDto : BaseDto { [MaxLength(512)] public string? Notes { get; set; } - public static DetailDto CreateDefaultDetailDto() { - var detailType = DetailTypeDto.CreateDefaultDetailTypeDto(); - return new DetailDto { DetailResourceId = Guid.NewGuid(), DetailType = detailType, Intensity = IntensityDto.CreateDefaultIntensityDto(detailType.ResourceId), Notes = "Notes\nmore notes" }; + public static DetailDto DefaultDetailDto() { + var detailType = DetailTypeDto.DefaultDetailTypeDto(); + return new DetailDto { + DetailResourceId = Guid.Parse("00000000-0000-0000-0000-000000000001"), + DetailType = detailType, + Intensity = IntensityDto.DefaultIntensityDto(detailType.ResourceId), + Notes = "Notes\nmore notes" + }; } internal override void CopyUserValues(T source) { diff --git a/EventJournal.DomainModels/DtoHelpers.cs b/EventJournal.DomainModels/DtoHelper.cs similarity index 96% rename from EventJournal.DomainModels/DtoHelpers.cs rename to EventJournal.DomainModels/DtoHelper.cs index c6d38a7..fdc5c64 100644 --- a/EventJournal.DomainModels/DtoHelpers.cs +++ b/EventJournal.DomainModels/DtoHelper.cs @@ -1,9 +1,8 @@ using EventJournal.DomainDto.UserTypes; -using System.Diagnostics; using System.Text.Json; namespace EventJournal.DomainDto { - public static partial class DtoHelpers { + public static partial class DtoHelper { public static T UpdateEntity(this T destination, T source) where T : BaseDto { destination.CopyUserValues(source); destination.CreatedDate = source.CreatedDate; diff --git a/EventJournal.DomainModels/EventDto.cs b/EventJournal.DomainModels/EventDto.cs index e797113..d82b477 100644 --- a/EventJournal.DomainModels/EventDto.cs +++ b/EventJournal.DomainModels/EventDto.cs @@ -6,10 +6,11 @@ public class EventDto : BaseDto { public Guid EventResourceId { get { return ResourceId; } set { ResourceId = value; } } [Required] - public EventTypeDto Type { get; set; } = EventTypeDto.CreateDefaultEventTypeDto(); + public EventTypeDto EventType { get; set; } = EventTypeDto.DefaultEventTypeDto(); [Required] public DateTime StartTime { get; set; } = DateTime.Now; + public DateTime? EndTime { get; set; } = null; [MaxLength(500)] @@ -17,12 +18,18 @@ public class EventDto : BaseDto { public IEnumerable Details { get; set; } = []; - public static EventDto CreateDefaultEventDto() { - return new EventDto { EventResourceId = Guid.NewGuid(), StartTime = DateTime.Now, EndTime = DateTime.Now.AddMinutes(1).AddSeconds(1), Description = "Description", Details = [ DetailDto.CreateDefaultDetailDto()] }; + public static EventDto DefaultEventDto() { + return new EventDto { + EventResourceId = Guid.Parse("00000000-0000-0000-0000-000000000001"), + StartTime = DateTime.Now, + EndTime = DateTime.Now.AddMinutes(1).AddSeconds(1), + Description = "Description" + }; } + internal override void CopyUserValues(T source) { var soruceEvent = source as EventDto ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(EventDto)}"); - Type = soruceEvent.Type; + EventType = soruceEvent.EventType; StartTime = soruceEvent.StartTime; EndTime = soruceEvent.EndTime; Description = soruceEvent.Description; diff --git a/EventJournal.DomainModels/UserTypes/DetailTypeDto.cs b/EventJournal.DomainModels/UserTypes/DetailTypeDto.cs index c9fcd1a..a7401d0 100644 --- a/EventJournal.DomainModels/UserTypes/DetailTypeDto.cs +++ b/EventJournal.DomainModels/UserTypes/DetailTypeDto.cs @@ -14,8 +14,12 @@ public class DetailTypeDto : BaseDto { public IEnumerable AllowedIntensities { get; set; } = []; //TODO: does this belong here?? - public static DetailTypeDto CreateDefaultDetailTypeDto() { - return new DetailTypeDto { DetailTypeResourceId = Guid.NewGuid(), Description = "Description", Name = "Default DTO Type" }; + public static DetailTypeDto DefaultDetailTypeDto() { + return new DetailTypeDto { + DetailTypeResourceId = Guid.Parse("00000000-0000-0000-0000-000000000001"), + Description = "Description", + Name = "Default DTO Type" + }; } internal override void CopyUserValues(T source) { diff --git a/EventJournal.DomainModels/UserTypes/EventTypeDto.cs b/EventJournal.DomainModels/UserTypes/EventTypeDto.cs index 9cf350b..dfd209e 100644 --- a/EventJournal.DomainModels/UserTypes/EventTypeDto.cs +++ b/EventJournal.DomainModels/UserTypes/EventTypeDto.cs @@ -13,17 +13,21 @@ public class EventTypeDto : BaseDto { //TODO: does this belong here?? public static IEnumerable DefaultEventTypes() { return [ - CreateDefaultEventTypeDto(), - new EventTypeDto { EventTypeResourceId = Guid.NewGuid(), Name = "Exercise" }, - new EventTypeDto { EventTypeResourceId = Guid.NewGuid(), Name = "Bathroom Visit" }, - new EventTypeDto { EventTypeResourceId = Guid.NewGuid(), Name = "Food Consumption" }, - new EventTypeDto { EventTypeResourceId = Guid.NewGuid(), Name = "WeighIn" }, + DefaultEventTypeDto(), + new EventTypeDto { EventTypeResourceId = Guid.Parse("00000000-0000-0000-0000-000000000001"), Name = "Exercise" }, + new EventTypeDto { EventTypeResourceId = Guid.Parse("00000000-0000-0000-0000-000000000002"), Name = "Bathroom Visit" }, + new EventTypeDto { EventTypeResourceId = Guid.Parse("00000000-0000-0000-0000-000000000003"), Name = "Food Consumption" }, + new EventTypeDto { EventTypeResourceId = Guid.Parse("00000000-0000-0000-0000-000000000004"), Name = "WeighIn" }, ]; } - //TODO: does this belong here?? - public static EventTypeDto CreateDefaultEventTypeDto() { - return new EventTypeDto { EventTypeResourceId = Guid.NewGuid(), Name = "Random Event", Description = "Use for tracking random things like onset of pain, headache, or whatever that isn't directly associated with a specific even type." }; + //TODO: does this belong here?? maybe should ensure default event type is created in the database at startup, and then use that instead of creating a new one here. + public static EventTypeDto DefaultEventTypeDto() { + return new EventTypeDto { + EventTypeResourceId = Guid.Parse("00000000-0000-0000-0000-000000000005"), + Name = "Random Event", + Description = "Use for tracking random things like onset of pain, headache, or whatever that isn't directly associated with a specific even type." + }; } internal override void CopyUserValues(T source) { diff --git a/EventJournal.DomainModels/UserTypes/IntensityDto.cs b/EventJournal.DomainModels/UserTypes/IntensityDto.cs index 24721d8..be874f6 100644 --- a/EventJournal.DomainModels/UserTypes/IntensityDto.cs +++ b/EventJournal.DomainModels/UserTypes/IntensityDto.cs @@ -21,8 +21,15 @@ public class IntensityDto : BaseDto { [Required] public required Guid DetailTypeId { get; set; } //TODO: does this belong here?? - public static IntensityDto CreateDefaultIntensityDto(Guid detailTypeId) { - return new IntensityDto { IntensityResourceId = Guid.NewGuid(), DefaultSortType = SortType.Descending, Level = 1, Name = "Default Intensity DTO", Description = "Description", DetailTypeId=detailTypeId }; + public static IntensityDto DefaultIntensityDto(Guid detailTypeId) { + return new IntensityDto { + IntensityResourceId = Guid.Parse("00000000-0000-0000-0000-000000000001"), + DefaultSortType = SortType.Descending, + Level = 1, + Name = "Default Intensity DTO", + Description = "Description", + DetailTypeId = detailTypeId + }; } internal override void CopyUserValues(T source) { diff --git a/EventJournal.DomainService/EventService.cs b/EventJournal.DomainService/EventService.cs index ddf5828..58ce2c8 100644 --- a/EventJournal.DomainService/EventService.cs +++ b/EventJournal.DomainService/EventService.cs @@ -20,7 +20,10 @@ public async Task> GetAllEventsAsync() { public async Task AddUpdateEventAsync(EventDto dto) { ArgumentNullException.ThrowIfNull(dto); //TODO: additional validations? - return mapper.Map(await eventRepository.AddUpdateAsync(mapper.Map(dto)).ConfigureAwait(false)); + var eventEntity = mapper.Map(dto); + eventEntity = await eventRepository.AddUpdateAsync(eventEntity).ConfigureAwait(false); + var resultDto = mapper.Map(eventEntity); + return resultDto; } public async Task DeleteEventAsync(Guid resourceId) { var entity = await eventRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false); From bd6915b0bf3adfb755142b2e50c3877f06b981b9 Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Tue, 19 Aug 2025 02:21:09 -0400 Subject: [PATCH 22/34] final? initial migration. cli menu works, basic crud for Event and Event Type work. --- EventJournal.Data/BaseRepository.cs | 16 +- EventJournal.Data/EventRepository.cs | 24 +- ...819054033_InitialMigrationTry3.Designer.cs | 252 ++++++++++++++++++ .../20250819054033_InitialMigrationTry3.cs | 181 +++++++++++++ .../DatabaseContextModelSnapshot.cs | 249 +++++++++++++++++ EventJournal.DomainModels/BaseDto.cs | 5 +- README.md | 26 +- 7 files changed, 738 insertions(+), 15 deletions(-) create mode 100644 EventJournal.Data/Migrations/20250819054033_InitialMigrationTry3.Designer.cs create mode 100644 EventJournal.Data/Migrations/20250819054033_InitialMigrationTry3.cs create mode 100644 EventJournal.Data/Migrations/DatabaseContextModelSnapshot.cs diff --git a/EventJournal.Data/BaseRepository.cs b/EventJournal.Data/BaseRepository.cs index 0edea45..645a41e 100644 --- a/EventJournal.Data/BaseRepository.cs +++ b/EventJournal.Data/BaseRepository.cs @@ -4,31 +4,31 @@ namespace EventJournal.Data { public abstract class BaseRepository(IDatabaseContext db, DbSet table) : IBaseRepository where T : BaseEntity { private readonly IDatabaseContext db = db; - private readonly DbSet table = table; - internal async Task AddAsync(T entity) { + internal readonly DbSet table = table; + internal virtual async Task AddAsync(T entity) { var row = await table.AddAsync(entity).ConfigureAwait(false); await db.SaveChangesAsync().ConfigureAwait(false); return row.Entity; } - public async Task> GetAllAsync() { + public virtual async Task> GetAllAsync() { return await table.ToListAsync().ConfigureAwait(false); } - internal async Task GetByIdAsync(int id) { + internal virtual async Task GetByIdAsync(int id) { return await table.FindAsync(id).ConfigureAwait(false); } - public Task GetByResourceIdAsync(Guid resourceId) { + public virtual Task GetByResourceIdAsync(Guid resourceId) { return table.FirstOrDefaultAsync(t => t.ResourceId == resourceId); } - - public Task DeleteAsync(T entity) { + + public virtual Task DeleteAsync(T entity) { table.Remove(entity); return db.SaveChangesAsync(); } - public async Task AddUpdateAsync(T source) { + public virtual async Task AddUpdateAsync(T source) { ArgumentException.ThrowIfNullOrEmpty(nameof(source)); var existingEntity = await GetByIdAsync(source.Id) ?? await GetByResourceIdAsync(source.ResourceId).ConfigureAwait(false); diff --git a/EventJournal.Data/EventRepository.cs b/EventJournal.Data/EventRepository.cs index da2475e..1fbe43c 100644 --- a/EventJournal.Data/EventRepository.cs +++ b/EventJournal.Data/EventRepository.cs @@ -1,6 +1,28 @@ using EventJournal.Data.Entities; +using Microsoft.EntityFrameworkCore; namespace EventJournal.Data { - public class EventRepository(IDatabaseContext db) : BaseRepository(db, db.Events), IEventRepository { + public sealed class EventRepository(IDatabaseContext db) : BaseRepository(db, db.Events), IEventRepository { + public override async Task> GetAllAsync() { + return await table + .Include(e => e.EventType) + .Include(e => e.Details) + .ThenInclude(d => d.DetailType) + .ThenInclude(dt => dt.AllowedIntensities) + .Include(e => e.Details) + .ThenInclude(d => d.Intensity) + .ToListAsync() + .ConfigureAwait(false); + } + public override Task GetByResourceIdAsync(Guid resourceId) { + return table + .Include(e => e.EventType) + .Include(e => e.Details) + .ThenInclude(d => d.DetailType) + .Include(e => e.Details) + .ThenInclude(d => d.Intensity) + .FirstOrDefaultAsync(e => e.ResourceId == resourceId) +; + } } } diff --git a/EventJournal.Data/Migrations/20250819054033_InitialMigrationTry3.Designer.cs b/EventJournal.Data/Migrations/20250819054033_InitialMigrationTry3.Designer.cs new file mode 100644 index 0000000..5d6361f --- /dev/null +++ b/EventJournal.Data/Migrations/20250819054033_InitialMigrationTry3.Designer.cs @@ -0,0 +1,252 @@ +// +using System; +using EventJournal.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace EventJournal.Data.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20250819054033_InitialMigrationTry3")] + partial class InitialMigrationTry3 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.8"); + + modelBuilder.Entity("EventJournal.Data.Entities.Detail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("DetailTypeId") + .HasColumnType("INTEGER"); + + b.Property("EventId") + .HasColumnType("INTEGER"); + + b.Property("IntensityId") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("ResourceId") + .HasColumnType("TEXT"); + + b.Property("UpdatedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DetailTypeId"); + + b.HasIndex("EventId"); + + b.HasIndex("IntensityId"); + + b.ToTable("Details"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("EndTime") + .HasColumnType("TEXT"); + + b.Property("EventTypeId") + .HasColumnType("INTEGER"); + + b.Property("ResourceId") + .HasColumnType("TEXT"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.Property("UpdatedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EventTypeId"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.DetailType", b => + { + b.Property("DetailTypeId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DetailTypeResourceId") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDate") + .HasColumnType("TEXT"); + + b.HasKey("DetailTypeId"); + + b.ToTable("DetailTypes"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.EventType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ResourceId") + .HasColumnType("TEXT"); + + b.Property("UpdatedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("EventTypes"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.Intensity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("DefaultSortType") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DetailTypeId") + .HasColumnType("INTEGER"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ResourceId") + .HasColumnType("TEXT"); + + b.Property("UpdatedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DetailTypeId"); + + b.ToTable("Intensities"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.Detail", b => + { + b.HasOne("EventJournal.Data.Entities.UserTypes.DetailType", "DetailType") + .WithMany() + .HasForeignKey("DetailTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("EventJournal.Data.Entities.Event", "Event") + .WithMany("Details") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("EventJournal.Data.Entities.UserTypes.Intensity", "Intensity") + .WithMany() + .HasForeignKey("IntensityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DetailType"); + + b.Navigation("Event"); + + b.Navigation("Intensity"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.Event", b => + { + b.HasOne("EventJournal.Data.Entities.UserTypes.EventType", "EventType") + .WithMany() + .HasForeignKey("EventTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EventType"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.Intensity", b => + { + b.HasOne("EventJournal.Data.Entities.UserTypes.DetailType", null) + .WithMany("AllowedIntensities") + .HasForeignKey("DetailTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.Event", b => + { + b.Navigation("Details"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.DetailType", b => + { + b.Navigation("AllowedIntensities"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/EventJournal.Data/Migrations/20250819054033_InitialMigrationTry3.cs b/EventJournal.Data/Migrations/20250819054033_InitialMigrationTry3.cs new file mode 100644 index 0000000..3a3e2ba --- /dev/null +++ b/EventJournal.Data/Migrations/20250819054033_InitialMigrationTry3.cs @@ -0,0 +1,181 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace EventJournal.Data.Migrations +{ + /// + public partial class InitialMigrationTry3 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "DetailTypes", + columns: table => new + { + DetailTypeId = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + DetailTypeResourceId = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + Description = table.Column(type: "TEXT", maxLength: 500, nullable: true), + CreatedDate = table.Column(type: "TEXT", nullable: false), + UpdatedDate = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_DetailTypes", x => x.DetailTypeId); + }); + + migrationBuilder.CreateTable( + name: "EventTypes", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ResourceId = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + Description = table.Column(type: "TEXT", maxLength: 500, nullable: true), + CreatedDate = table.Column(type: "TEXT", nullable: false), + UpdatedDate = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_EventTypes", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Intensities", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ResourceId = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 50, nullable: false), + Level = table.Column(type: "INTEGER", nullable: false), + Description = table.Column(type: "TEXT", maxLength: 500, nullable: true), + DefaultSortType = table.Column(type: "INTEGER", nullable: false), + DetailTypeId = table.Column(type: "INTEGER", nullable: false), + CreatedDate = table.Column(type: "TEXT", nullable: false), + UpdatedDate = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Intensities", x => x.Id); + table.ForeignKey( + name: "FK_Intensities_DetailTypes_DetailTypeId", + column: x => x.DetailTypeId, + principalTable: "DetailTypes", + principalColumn: "DetailTypeId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Events", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ResourceId = table.Column(type: "TEXT", nullable: false), + EventTypeId = table.Column(type: "INTEGER", nullable: false), + StartTime = table.Column(type: "TEXT", nullable: false), + EndTime = table.Column(type: "TEXT", nullable: true), + Description = table.Column(type: "TEXT", maxLength: 500, nullable: true), + CreatedDate = table.Column(type: "TEXT", nullable: false), + UpdatedDate = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Events", x => x.Id); + table.ForeignKey( + name: "FK_Events_EventTypes_EventTypeId", + column: x => x.EventTypeId, + principalTable: "EventTypes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Details", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ResourceId = table.Column(type: "TEXT", nullable: false), + EventId = table.Column(type: "INTEGER", nullable: false), + DetailTypeId = table.Column(type: "INTEGER", nullable: false), + IntensityId = table.Column(type: "INTEGER", nullable: false), + Notes = table.Column(type: "TEXT", maxLength: 512, nullable: true), + CreatedDate = table.Column(type: "TEXT", nullable: false), + UpdatedDate = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Details", x => x.Id); + table.ForeignKey( + name: "FK_Details_DetailTypes_DetailTypeId", + column: x => x.DetailTypeId, + principalTable: "DetailTypes", + principalColumn: "DetailTypeId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Details_Events_EventId", + column: x => x.EventId, + principalTable: "Events", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Details_Intensities_IntensityId", + column: x => x.IntensityId, + principalTable: "Intensities", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Details_DetailTypeId", + table: "Details", + column: "DetailTypeId"); + + migrationBuilder.CreateIndex( + name: "IX_Details_EventId", + table: "Details", + column: "EventId"); + + migrationBuilder.CreateIndex( + name: "IX_Details_IntensityId", + table: "Details", + column: "IntensityId"); + + migrationBuilder.CreateIndex( + name: "IX_Events_EventTypeId", + table: "Events", + column: "EventTypeId"); + + migrationBuilder.CreateIndex( + name: "IX_Intensities_DetailTypeId", + table: "Intensities", + column: "DetailTypeId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Details"); + + migrationBuilder.DropTable( + name: "Events"); + + migrationBuilder.DropTable( + name: "Intensities"); + + migrationBuilder.DropTable( + name: "EventTypes"); + + migrationBuilder.DropTable( + name: "DetailTypes"); + } + } +} diff --git a/EventJournal.Data/Migrations/DatabaseContextModelSnapshot.cs b/EventJournal.Data/Migrations/DatabaseContextModelSnapshot.cs new file mode 100644 index 0000000..a7a622d --- /dev/null +++ b/EventJournal.Data/Migrations/DatabaseContextModelSnapshot.cs @@ -0,0 +1,249 @@ +// +using System; +using EventJournal.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace EventJournal.Data.Migrations +{ + [DbContext(typeof(DatabaseContext))] + partial class DatabaseContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.8"); + + modelBuilder.Entity("EventJournal.Data.Entities.Detail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("DetailTypeId") + .HasColumnType("INTEGER"); + + b.Property("EventId") + .HasColumnType("INTEGER"); + + b.Property("IntensityId") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("ResourceId") + .HasColumnType("TEXT"); + + b.Property("UpdatedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DetailTypeId"); + + b.HasIndex("EventId"); + + b.HasIndex("IntensityId"); + + b.ToTable("Details"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("EndTime") + .HasColumnType("TEXT"); + + b.Property("EventTypeId") + .HasColumnType("INTEGER"); + + b.Property("ResourceId") + .HasColumnType("TEXT"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.Property("UpdatedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EventTypeId"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.DetailType", b => + { + b.Property("DetailTypeId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DetailTypeResourceId") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDate") + .HasColumnType("TEXT"); + + b.HasKey("DetailTypeId"); + + b.ToTable("DetailTypes"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.EventType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ResourceId") + .HasColumnType("TEXT"); + + b.Property("UpdatedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("EventTypes"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.Intensity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("DefaultSortType") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DetailTypeId") + .HasColumnType("INTEGER"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ResourceId") + .HasColumnType("TEXT"); + + b.Property("UpdatedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DetailTypeId"); + + b.ToTable("Intensities"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.Detail", b => + { + b.HasOne("EventJournal.Data.Entities.UserTypes.DetailType", "DetailType") + .WithMany() + .HasForeignKey("DetailTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("EventJournal.Data.Entities.Event", "Event") + .WithMany("Details") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("EventJournal.Data.Entities.UserTypes.Intensity", "Intensity") + .WithMany() + .HasForeignKey("IntensityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DetailType"); + + b.Navigation("Event"); + + b.Navigation("Intensity"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.Event", b => + { + b.HasOne("EventJournal.Data.Entities.UserTypes.EventType", "EventType") + .WithMany() + .HasForeignKey("EventTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EventType"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.Intensity", b => + { + b.HasOne("EventJournal.Data.Entities.UserTypes.DetailType", null) + .WithMany("AllowedIntensities") + .HasForeignKey("DetailTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.Event", b => + { + b.Navigation("Details"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.DetailType", b => + { + b.Navigation("AllowedIntensities"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/EventJournal.DomainModels/BaseDto.cs b/EventJournal.DomainModels/BaseDto.cs index d0967db..f91db33 100644 --- a/EventJournal.DomainModels/BaseDto.cs +++ b/EventJournal.DomainModels/BaseDto.cs @@ -1,10 +1,13 @@ -namespace EventJournal.DomainDto { +using System.Text.Json.Serialization; + +namespace EventJournal.DomainDto { public abstract class BaseDto { public DateTime CreatedDate { get; set; } = DateTime.UtcNow; public DateTime UpdatedDate { get; set; } = DateTime.UtcNow; + [JsonIgnore] public Guid ResourceId { get; set; } /// diff --git a/README.md b/README.md index f7fb53c..6fb7d60 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,19 @@ Collect basic timestamped event data with the intent that the data can be analyz Originally this was intended to track symptoms and intensity. But evolved into more of an event tracker system. The concept of Intensity is still a bit muddy, but the idea is that events of a particular even type can be rated on a user defined scale. For example a headache event can have a pain intensity rating. Where an exercise type event could have a distance or workout intensity. - + + - [Features](#features) + - [NOTES](#notes) + - [Entities](#entities) + - [Intensity Examples](#intensity-examples) + - [Pain level could be something like](#pain-level-could-be-something-like) + - [Bleeding could be](#bleeding-could-be) + - [Exercise Intensity could be](#exercise-intensity-could-be) + - [Entity Framework help](#entity-framework-help) + - [NEXT STEPS](#next-steps) + - [TODO](#todo) + - [Future Considerations](#future-considerations) + ## Features - User definable event types (ex. exercise, meal, headache ) @@ -49,12 +61,16 @@ EF https://learn.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs ### NEXT STEPS -- add default data from CLI isn't working - - looks like you have to add detail type first, then add intensities - - then you can add everything else? +- Add details, detail types, and intensities to the initializer +- update detail, detail type repositories to return related entities + - detail should include detail type and intensity and detailtypes.AllowedIntensities + - detail type should include valid intensities +- test full get event with details +- test full get detail type with allowed intensities +- add multiple default detail types to initializer (a la event types) +- add multiple default intensities to initializer (a la event types) ### TODO - unit tests for repositories and services. Also base entity code? -- CLI interface to call service methods - Web API to call service methods - Add common BootStrap code to be consumed by cli and web api projects - remove microsoft.extension.hosting pkg where not needed From 440c57b68e9f86209118bd0e3ea5e4bf333da405 Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Sun, 24 Aug 2025 02:46:24 -0400 Subject: [PATCH 23/34] data restructure, adds almost work except need to move defaults back down into the data layer --- EventJournal.CLI/EventJournal.CLI.csproj | 1 + EventJournal.CLI/Program.cs | 200 +++++--------- EventJournal.Data/BaseRepository.cs | 31 ++- EventJournal.Data/DetailRepository.cs | 16 ++ EventJournal.Data/Entities/BaseEntity.cs | 13 +- EventJournal.Data/Entities/Detail.cs | 4 +- EventJournal.Data/Entities/EntityHelper.cs | 6 +- .../Entities/Enumerations/SortType.cs | 5 +- EventJournal.Data/Entities/Event.cs | 8 +- .../Entities/UserTypes/DetailType.cs | 12 +- .../Entities/UserTypes/EventType.cs | 4 +- .../Entities/UserTypes/Intensity.cs | 17 +- EventJournal.Data/EventRepository.cs | 4 +- EventJournal.Data/IBaseRepository.cs | 5 +- ...0824035035_InitialMigration.V5.Designer.cs | 250 ++++++++++++++++++ ... => 20250824035035_InitialMigration.V5.cs} | 17 +- ...ypeIntensityRelationshipAgain.Designer.cs} | 22 +- ...fixDetailTypeIntensityRelationshipAgain.cs | 59 +++++ .../DatabaseContextModelSnapshot.cs | 18 +- .../DetailTypeRepository.cs | 13 + .../IntensityRepository.cs | 11 + EventJournal.DomainModels/BaseDto.cs | 2 +- EventJournal.DomainModels/DetailDto.cs | 24 +- EventJournal.DomainModels/DtoHelper.cs | 42 ++- .../Enumerations/SortType.cs | 5 +- EventJournal.DomainModels/EventDto.cs | 23 +- .../UserTypes/DetailTypeDto.cs | 30 ++- .../UserTypes/EventTypeDto.cs | 35 ++- .../UserTypes/IntensityDto.cs | 68 +++-- EventJournal.DomainService/DetailService.cs | 123 +++++++++ EventJournal.DomainService/EventService.cs | 80 ++++-- .../Exceptions/ResourceNotFoundException.cs | 4 + EventJournal.DomainService/IDetailService.cs | 30 +++ EventJournal.DomainService/IEventService.cs | 15 +- .../IUserTypeService.cs | 22 -- EventJournal.DomainService/UserTypeService.cs | 65 ----- EventJournal.Models/EventDataResponseModel.cs | 10 + .../EventJournal.PublicModels.csproj | 13 + EventJournal.sln | 6 + README.md | 16 +- 40 files changed, 909 insertions(+), 420 deletions(-) create mode 100644 EventJournal.Data/Migrations/20250824035035_InitialMigration.V5.Designer.cs rename EventJournal.Data/Migrations/{20250819054033_InitialMigrationTry3.cs => 20250824035035_InitialMigration.V5.cs} (92%) rename EventJournal.Data/Migrations/{20250819054033_InitialMigrationTry3.Designer.cs => 20250824044228_fixDetailTypeIntensityRelationshipAgain.Designer.cs} (95%) create mode 100644 EventJournal.Data/Migrations/20250824044228_fixDetailTypeIntensityRelationshipAgain.cs create mode 100644 EventJournal.DomainService/DetailService.cs create mode 100644 EventJournal.DomainService/IDetailService.cs delete mode 100644 EventJournal.DomainService/IUserTypeService.cs delete mode 100644 EventJournal.DomainService/UserTypeService.cs create mode 100644 EventJournal.Models/EventDataResponseModel.cs create mode 100644 EventJournal.Models/EventJournal.PublicModels.csproj diff --git a/EventJournal.CLI/EventJournal.CLI.csproj b/EventJournal.CLI/EventJournal.CLI.csproj index dbc82e0..368bebc 100644 --- a/EventJournal.CLI/EventJournal.CLI.csproj +++ b/EventJournal.CLI/EventJournal.CLI.csproj @@ -28,5 +28,6 @@ + diff --git a/EventJournal.CLI/Program.cs b/EventJournal.CLI/Program.cs index 69efc14..49401de 100644 --- a/EventJournal.CLI/Program.cs +++ b/EventJournal.CLI/Program.cs @@ -2,17 +2,18 @@ using EventJournal.CLI; using EventJournal.Data; using EventJournal.Data.UserTypeRepositories; -using EventJournal.DomainDto; using EventJournal.DomainService; +using EventJournal.PublicModels; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using System.Text.Json; internal class Program { private static async Task Main(string[] args) { var services = CreateServiceCollection(); var eventService = services.GetService() ?? throw new Exception("Unable to locate a valid Product Logic module"); - var userTypeService = services.GetService() ?? throw new Exception("Unable to locate a valid Order Logic module"); + var userTypeService = services.GetService() ?? throw new Exception("Unable to locate a valid Order Logic module"); bool userIsDone = false; while (!userIsDone) { //Console.WriteLine("Type '1' to "); @@ -61,7 +62,7 @@ private static async Task Main(string[] args) { //case '9': // break; case 'v': - await ViewallDataAsync().ConfigureAwait(false); + await ViewallDataAsync(GetSerializerOptions(), eventService, userTypeService).ConfigureAwait(false); break; case 'a': await AddTestDataAsync(eventService, userTypeService).ConfigureAwait(false); @@ -70,148 +71,77 @@ private static async Task Main(string[] args) { await DeleteAllDataAsync(eventService, userTypeService).ConfigureAwait(false); break; } - Console.WriteLine("\n===============================\n"); + Console.WriteLine("\n=================================================\n"); } - //TODO: move to shared startup.cs and remove this method and remove microsoft.extension.hosting pkg - static IServiceProvider CreateServiceCollection() { - var servicecollection = new ServiceCollection() - .AddDbContext(options => { - options.UseSqlite($"Data Source={DatabaseContext.GetSqliteDbPath()}"); - }) - .AddAutoMapper(cfg => { }, typeof(DomainMapperProfile)) - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddLogging(options => { - options.AddDebug(); - options.SetMinimumLevel(LogLevel.Error); - options.AddSimpleConsole(options => { - options.SingleLine = true; - options.TimestampFormat = "HH:mm:ss.fff "; - options.ColorBehavior = Microsoft.Extensions.Logging.Console.LoggerColorBehavior.Enabled; - }); + } + //TODO: move to shared bootsrapper.cs and remove this method and remove microsoft.extension.hosting pkg + static IServiceProvider CreateServiceCollection() { + var servicecollection = new ServiceCollection() + .AddDbContext(options => { + options.UseSqlite($"Data Source={DatabaseContext.GetSqliteDbPath()}"); + }) + .AddAutoMapper(cfg => { }, typeof(DomainMapperProfile)) + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddLogging(options => { + options.AddDebug(); + options.SetMinimumLevel(LogLevel.Error); + options.AddSimpleConsole(options => { + options.SingleLine = true; + options.TimestampFormat = "HH:mm:ss.fff "; + options.ColorBehavior = Microsoft.Extensions.Logging.Console.LoggerColorBehavior.Enabled; }); + }); + + return servicecollection.BuildServiceProvider(); + } + static async Task AddTestDataAsync(IEventService eventService, IDetailService userTypeService) { + Console.WriteLine("Adding/Resetting test data."); + await eventService.AddTestDataAsync().ConfigureAwait(false); + await userTypeService.AddTestDataAsync().ConfigureAwait(false); + } + static async Task DeleteAllDataAsync(IEventService eventService, IDetailService userTypeService) { - return servicecollection.BuildServiceProvider(); + Console.WriteLine("Deleting all data."); + foreach (var e in await eventService.GetAllEventsAsync().ConfigureAwait(false)) { + await eventService.DeleteEventAsync(e.ResourceId).ConfigureAwait(false); } - async Task AddTestDataAsync(IEventService eventService, IUserTypeService userTypeService) { - Console.WriteLine("Adding/Resetting test data."); - await AddUpdateDtoAsync(EventDto.DefaultEventDto()).ConfigureAwait(false); - //await AddUpdateEntity(new Product { ProductId = 2, Name = "Dry Cat Food", Quantity = 0, Price = 15.99M }).ConfigureAwait(false); - //await AddUpdateEntity(new Product { ProductId = 100, Name = "Designer Leash", Quantity = 1, Price = 99.99M }).ConfigureAwait(false); - //await AddUpdateEntity(new Order { OrderId = 1, OrderDate = DateTime.Now, OrderProducts = { new OrderProduct { ProductId = 100, OrderQuantity = 5, UnitPrice = 99.99M } } }).ConfigureAwait(false); - //await AddUpdateEntity(new Order { OrderId = 2, OrderDate = DateTime.Now, OrderProducts = { new OrderProduct { ProductId = 2, OrderQuantity = 3, UnitPrice = 15.99M } } }).ConfigureAwait(false); + foreach (var e in await eventService.GetAllEventTypesAsync().ConfigureAwait(false)) { + await eventService.DeleteEventTypeAsync(e.ResourceId).ConfigureAwait(false); } - static async Task DeleteAllDataAsync(IEventService eventService, IUserTypeService userTypeService) { - var events = await eventService.GetAllEventsAsync().ConfigureAwait(false); - foreach (var e in events) { - await eventService.DeleteEventAsync(e.EventResourceId).ConfigureAwait(false); - } - //var orders =( await userTypeService.GetOrdersAsync().ConfigureAwait(false)).ToList(); - //orders.ForEach(async o => await orderService.RemoveOrderAsync(o).ConfigureAwait(false)); + foreach (var e in await userTypeService.GetAllDetailsAsync().ConfigureAwait(false)) { + await userTypeService.DeleteDetailAsync(e.ResourceId).ConfigureAwait(false); } - async Task ViewallDataAsync() { - var events = await eventService.GetAllEventsAsync().ConfigureAwait(false); - foreach (var e in events) { - Console.WriteLine(e.Serialize()); - } + foreach (var e in await userTypeService.GetAllIntensitiesAsync().ConfigureAwait(false)) { + await userTypeService.DeleteIntensityAsync(e.ResourceId).ConfigureAwait(false); } - //static T? GetEntityFromUser() where T : EntityBase { - // var json = CLIUtilities.GetStringFromUser($"Enter {typeof(T)} JSON: "); - // T? entity = json.Deserialize(); - // if (entity == null) { - // Console.WriteLine("Invalid JSON."); - // return null; - // } - // return entity; - //} - - //static bool ValidateEntity(T entity) where T : EntityBase { - // var validationResult = entity.Validate(); - // if (!validationResult.IsValid) { - // Console.WriteLine("Invalid Product data."); - // foreach (var error in validationResult.Errors) { - // Console.WriteLine(error); - // } - // return false; - // } - // return true; - //} - - //async Task AddEntity(T? newEntity) where T : EntityBase { - // if (newEntity == null) { - // Console.WriteLine("Nothing added."); - // return; - // } - // if (!ValidateEntity(newEntity)) { - // return; - // } - - // if (newEntity is Product newProduct) { - // await ProductService.AddProductAsync(newProduct); - // Console.WriteLine($"Added {newProduct.Name}."); - // return; - // } - - // if (newEntity is Order newOrder) { - // await OrderService.AddOrderAsync(newOrder); - // Console.WriteLine($"Added order # {newOrder.OrderId}."); - // } - //} - - //async Task ViewProduct() { - // var name = CLIUtilities.GetStringFromUser("Enter the product name you want to view: "); - // var product = await ProductService.GetProductAsync(name).ConfigureAwait(false); - // if (product == null) { - // Console.WriteLine($"The product '{name}' was not found.\n"); - // return; - // } - // Console.WriteLine(product.Serialize()); - // Console.WriteLine(); - //} - - //async Task ViewOrder() { - // var orderId = CLIUtilities.GetIntFromUser("Enter the order id you want to view: "); - // var order = await OrderService.GetOrderAsync(orderId).ConfigureAwait(false); - // if (order == null) { - // Console.WriteLine($"Order Id {orderId} was not found.\n"); - // return; - // } - // Console.WriteLine(order.ToString()); - // Console.WriteLine(); - //} - - - //async Task ViewInStockProducts() { - // var inStock = await ProductService.GetInStockProductsAsync().ConfigureAwait(false); - // Console.WriteLine("The following products are in stock: "); - // inStock.ForEach(p => Console.WriteLine(p.Serialize())); - //} - - //async Task ViewAllProduct() { - // var products = await ProductService.GetProductsAsync().ConfigureAwait(false); - // products.ForEach(p => Console.WriteLine(p.Serialize())); - //} - - async Task AddUpdateDtoAsync(T? dtoUpdate) where T : BaseDto { + foreach (var e in await userTypeService.GetAllDetailTypesAsync().ConfigureAwait(false)) { + await userTypeService.DeleteDetailTypeAsync(e.ResourceId).ConfigureAwait(false); + } + } - if (dtoUpdate == null) { - Console.WriteLine("Nothing updated."); - return; - } + //TODO: move to shared utilities class or bootstrapper class and remove this method + private static JsonSerializerOptions GetSerializerOptions() { + return new JsonSerializerOptions { + WriteIndented = true, + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + } - //if (!ValidateEntity(entityUpdate)) { - // return; - //} + static async Task ViewallDataAsync(JsonSerializerOptions options, IEventService eventService, IDetailService userTypeService) { + EventDataResponseModel eventData = new() { + DetailTypes = await userTypeService.GetAllDetailTypesAsync().ConfigureAwait(false), + EventTypes = await eventService.GetAllEventTypesAsync().ConfigureAwait(false), + Events = await eventService.GetAllEventsAsync().ConfigureAwait(false) + }; - if (dtoUpdate is EventDto @event) { - await eventService.AddUpdateEventAsync(@event); - } - } + Console.WriteLine(JsonSerializer.Serialize(eventData, options)); } } \ No newline at end of file diff --git a/EventJournal.Data/BaseRepository.cs b/EventJournal.Data/BaseRepository.cs index 645a41e..a6b9515 100644 --- a/EventJournal.Data/BaseRepository.cs +++ b/EventJournal.Data/BaseRepository.cs @@ -7,7 +7,6 @@ public abstract class BaseRepository(IDatabaseContext db, DbSet table) : I internal readonly DbSet table = table; internal virtual async Task AddAsync(T entity) { var row = await table.AddAsync(entity).ConfigureAwait(false); - await db.SaveChangesAsync().ConfigureAwait(false); return row.Entity; } @@ -22,24 +21,34 @@ public virtual async Task> GetAllAsync() { public virtual Task GetByResourceIdAsync(Guid resourceId) { return table.FirstOrDefaultAsync(t => t.ResourceId == resourceId); } - - public virtual Task DeleteAsync(T entity) { + + public virtual void Delete(T entity) { table.Remove(entity); - return db.SaveChangesAsync(); } public virtual async Task AddUpdateAsync(T source) { ArgumentException.ThrowIfNullOrEmpty(nameof(source)); - var existingEntity = await GetByIdAsync(source.Id) - ?? await GetByResourceIdAsync(source.ResourceId).ConfigureAwait(false); - if (existingEntity == null) { - return await AddAsync(source).ConfigureAwait(false); + var entity = await GetByIdAsync(source.Id) ?? await GetByResourceIdAsync(source.ResourceId).ConfigureAwait(false); + if (entity == null) { + entity = await AddAsync(source).ConfigureAwait(false); + } else { + entity.UpdateEntity(source); } + return entity; + } - existingEntity.UpdateEntity(source); + public virtual async Task> AddUpdateAsync(IEnumerable sources) { + ArgumentException.ThrowIfNullOrEmpty(nameof(sources)); + var result = new List(); + foreach (var source in sources) { + var updatedEntity = await AddUpdateAsync(source).ConfigureAwait(false); + result.Add(updatedEntity); + } + return result; + } - await db.SaveChangesAsync().ConfigureAwait(false); - return existingEntity; + public Task SaveChangesAsync() { + return db.SaveChangesAsync(); } } } diff --git a/EventJournal.Data/DetailRepository.cs b/EventJournal.Data/DetailRepository.cs index 2362327..824ad14 100644 --- a/EventJournal.Data/DetailRepository.cs +++ b/EventJournal.Data/DetailRepository.cs @@ -1,6 +1,22 @@ using EventJournal.Data.Entities; +using Microsoft.EntityFrameworkCore; namespace EventJournal.Data { public class DetailRepository(IDatabaseContext db) : BaseRepository(db, db.Details), IDetailRepository { + public override async Task> GetAllAsync() { + return await table + .Include(d => d.DetailType) + .ThenInclude(dt => dt.AllowedIntensities) + .Include(d => d.Intensity) + .ToListAsync() + .ConfigureAwait(false); + } + public override Task GetByResourceIdAsync(Guid resourceId) { + return table + .Include(d => d.DetailType) + .ThenInclude(dt => dt.AllowedIntensities) + .Include(d => d.Intensity) + .FirstOrDefaultAsync(d => d.ResourceId == resourceId); + } } } diff --git a/EventJournal.Data/Entities/BaseEntity.cs b/EventJournal.Data/Entities/BaseEntity.cs index 7fc4e6a..b1b144c 100644 --- a/EventJournal.Data/Entities/BaseEntity.cs +++ b/EventJournal.Data/Entities/BaseEntity.cs @@ -1,21 +1,18 @@ using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; namespace EventJournal.Data.Entities { public abstract class BaseEntity { [Required] public DateTime CreatedDate { get; set; } = DateTime.UtcNow; - + [Required] public DateTime UpdatedDate { get; set; } = DateTime.UtcNow; - - [NotMapped] + [Required] - public int Id { get; set; } - - [NotMapped] + public abstract int Id { get; set; } + [Required] - public Guid ResourceId { get; set; } + public abstract Guid ResourceId { get; set; } /// /// This method is predominantly for updating an entity based on the values in another entity. diff --git a/EventJournal.Data/Entities/Detail.cs b/EventJournal.Data/Entities/Detail.cs index 39b4a6a..04c1bfe 100644 --- a/EventJournal.Data/Entities/Detail.cs +++ b/EventJournal.Data/Entities/Detail.cs @@ -4,10 +4,10 @@ namespace EventJournal.Data.Entities { public class Detail : BaseEntity { [Key] - public new int Id { get { return base.Id; } set { base.Id = value; } } + public override int Id { get; set; } [Required] - public new Guid ResourceId { get { return base.ResourceId; } set { base.ResourceId = value; } } + public override Guid ResourceId { get; set; } [Required] public required Event Event { get; set; } diff --git a/EventJournal.Data/Entities/EntityHelper.cs b/EventJournal.Data/Entities/EntityHelper.cs index 55fb593..3834aed 100644 --- a/EventJournal.Data/Entities/EntityHelper.cs +++ b/EventJournal.Data/Entities/EntityHelper.cs @@ -1,8 +1,12 @@ namespace EventJournal.Data.Entities { public static class EntityHelper { public static T UpdateEntity(this T destination, T source) where T : BaseEntity { + ArgumentNullException.ThrowIfNull(destination); + ArgumentNullException.ThrowIfNull(source); + if (destination.ResourceId != source.ResourceId) + //TODO: custom exception that supports a ThrowIf parameter? + throw new InvalidOperationException("ResourceIds do not match"); destination.CopyUserValues(source); - destination.CreatedDate = source.CreatedDate; destination.UpdatedDate = DateTime.UtcNow; return destination; } diff --git a/EventJournal.Data/Entities/Enumerations/SortType.cs b/EventJournal.Data/Entities/Enumerations/SortType.cs index 127eaa4..bd196c3 100644 --- a/EventJournal.Data/Entities/Enumerations/SortType.cs +++ b/EventJournal.Data/Entities/Enumerations/SortType.cs @@ -1,8 +1,9 @@ namespace EventJournal.Data.Entities.Enumerations { //TODO: why duplicate this enum? (other is in .DomainDto.Enumerations ) public enum SortType { - Custom, + None, Ascending, - Descending + Descending, + Custom } } diff --git a/EventJournal.Data/Entities/Event.cs b/EventJournal.Data/Entities/Event.cs index 6a0ed98..f501451 100644 --- a/EventJournal.Data/Entities/Event.cs +++ b/EventJournal.Data/Entities/Event.cs @@ -4,10 +4,10 @@ namespace EventJournal.Data.Entities { public class Event : BaseEntity { [Key] - public new int Id { get { return base.Id; } set { base.Id = value; } } + public override int Id { get; set; } [Required] - public new Guid ResourceId { get { return base.ResourceId; } set { base.ResourceId = value; } } + public override Guid ResourceId { get; set; } [Required] public required EventType EventType { get; set; } @@ -19,11 +19,11 @@ public class Event : BaseEntity { [MaxLength(500)] public string? Description { get; set; } - public IEnumerable Details { get; set; } = []; + public ICollection Details { get; set; } = []; internal override void CopyUserValues(T source) { var soruceEvent = source as Event ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(Event)}"); - EventType = soruceEvent.EventType; + EventType.UpdateEntity(soruceEvent.EventType); StartTime = soruceEvent.StartTime; EndTime = soruceEvent.EndTime; Description = soruceEvent.Description; diff --git a/EventJournal.Data/Entities/UserTypes/DetailType.cs b/EventJournal.Data/Entities/UserTypes/DetailType.cs index cc3a852..8d9ad08 100644 --- a/EventJournal.Data/Entities/UserTypes/DetailType.cs +++ b/EventJournal.Data/Entities/UserTypes/DetailType.cs @@ -1,12 +1,13 @@ -using System.ComponentModel.DataAnnotations; +using EventJournal.Data.Entities.Enumerations; +using System.ComponentModel.DataAnnotations; namespace EventJournal.Data.Entities.UserTypes { public class DetailType : BaseEntity { [Key] - public int DetailTypeId { get { return Id; } set { Id = value; } } + public override int Id { get; set; } [Required] - public Guid DetailTypeResourceId { get { return ResourceId; } set { ResourceId = value; } } + public override Guid ResourceId { get; set; } [Required] public required string Name { get; set; } @@ -14,7 +15,10 @@ public class DetailType : BaseEntity { [MaxLength(500)] public string? Description { get; set; } - public IEnumerable AllowedIntensities { get; set; } = []; + [Required] + public SortType IntensitySortType { get; set; } = SortType.None; + + public ICollection AllowedIntensities { get; set; } = []; internal override void CopyUserValues(T source) { var sourceDetailType = source as DetailType ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(DetailType)}"); diff --git a/EventJournal.Data/Entities/UserTypes/EventType.cs b/EventJournal.Data/Entities/UserTypes/EventType.cs index 40f103e..1b86018 100644 --- a/EventJournal.Data/Entities/UserTypes/EventType.cs +++ b/EventJournal.Data/Entities/UserTypes/EventType.cs @@ -3,10 +3,10 @@ namespace EventJournal.Data.Entities.UserTypes { public class EventType : BaseEntity { [Key] - public new int Id { get { return base.Id; } set { base.Id = value; } } + public override int Id { get; set; } [Required] - public new Guid ResourceId { get { return base.ResourceId; } set { base.ResourceId = value; } } + public override Guid ResourceId { get; set; } [Required] public required string Name { get; set; } diff --git a/EventJournal.Data/Entities/UserTypes/Intensity.cs b/EventJournal.Data/Entities/UserTypes/Intensity.cs index 337d0ec..9b28528 100644 --- a/EventJournal.Data/Entities/UserTypes/Intensity.cs +++ b/EventJournal.Data/Entities/UserTypes/Intensity.cs @@ -1,13 +1,11 @@ -using EventJournal.Data.Entities.Enumerations; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; namespace EventJournal.Data.Entities.UserTypes { public class Intensity : BaseEntity { [Key] - public new int Id { get { return base.Id; } set { base.Id = value; } } + public override int Id { get; set; } [Required] - public new Guid ResourceId { get { return base.ResourceId; } set { base.ResourceId = value; } } + public override Guid ResourceId { get; set; } [Required, MaxLength(50)] public required string Name { get; set; } @@ -19,19 +17,14 @@ public class Intensity : BaseEntity { public string? Description { get; set; } [Required] - public required SortType DefaultSortType { get; set; } - - [ForeignKey(nameof(DetailTypeId))] - [Required] - public required int DetailTypeId { get; set; } + public required DetailType DetailType { get; set; } internal override void CopyUserValues(T source) { var sourceIntensity = source as Intensity ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(Intensity)}"); Name = sourceIntensity.Name; Level = sourceIntensity.Level; Description = sourceIntensity.Description; - DefaultSortType = sourceIntensity.DefaultSortType; - DetailTypeId = sourceIntensity.DetailTypeId; + DetailType.CopyUserValues( sourceIntensity.DetailType); } } diff --git a/EventJournal.Data/EventRepository.cs b/EventJournal.Data/EventRepository.cs index 1fbe43c..9d55801 100644 --- a/EventJournal.Data/EventRepository.cs +++ b/EventJournal.Data/EventRepository.cs @@ -14,6 +14,7 @@ public override async Task> GetAllAsync() { .ToListAsync() .ConfigureAwait(false); } + public override Task GetByResourceIdAsync(Guid resourceId) { return table .Include(e => e.EventType) @@ -21,8 +22,7 @@ public override async Task> GetAllAsync() { .ThenInclude(d => d.DetailType) .Include(e => e.Details) .ThenInclude(d => d.Intensity) - .FirstOrDefaultAsync(e => e.ResourceId == resourceId) -; + .FirstOrDefaultAsync(e => e.ResourceId == resourceId); } } } diff --git a/EventJournal.Data/IBaseRepository.cs b/EventJournal.Data/IBaseRepository.cs index 3f31764..4de4d14 100644 --- a/EventJournal.Data/IBaseRepository.cs +++ b/EventJournal.Data/IBaseRepository.cs @@ -5,6 +5,9 @@ public interface IBaseRepository where T : BaseEntity { Task> GetAllAsync(); Task GetByResourceIdAsync(Guid resourceId); Task AddUpdateAsync(T source); - Task DeleteAsync(T entity); + Task> AddUpdateAsync(IEnumerable sources); + void Delete(T entity); + + Task SaveChangesAsync(); } } \ No newline at end of file diff --git a/EventJournal.Data/Migrations/20250824035035_InitialMigration.V5.Designer.cs b/EventJournal.Data/Migrations/20250824035035_InitialMigration.V5.Designer.cs new file mode 100644 index 0000000..e8525e0 --- /dev/null +++ b/EventJournal.Data/Migrations/20250824035035_InitialMigration.V5.Designer.cs @@ -0,0 +1,250 @@ +// +using System; +using EventJournal.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace EventJournal.Data.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20250824035035_InitialMigration.V5")] + partial class InitialMigrationV5 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.8"); + + modelBuilder.Entity("EventJournal.Data.Entities.Detail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("DetailTypeId") + .HasColumnType("INTEGER"); + + b.Property("EventId") + .HasColumnType("INTEGER"); + + b.Property("IntensityId") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("ResourceId") + .HasColumnType("TEXT"); + + b.Property("UpdatedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DetailTypeId"); + + b.HasIndex("EventId"); + + b.HasIndex("IntensityId"); + + b.ToTable("Details"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("EndTime") + .HasColumnType("TEXT"); + + b.Property("EventTypeId") + .HasColumnType("INTEGER"); + + b.Property("ResourceId") + .HasColumnType("TEXT"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.Property("UpdatedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EventTypeId"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.DetailType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("IntensitySortType") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ResourceId") + .HasColumnType("TEXT"); + + b.Property("UpdatedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("DetailTypes"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.EventType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ResourceId") + .HasColumnType("TEXT"); + + b.Property("UpdatedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("EventTypes"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.Intensity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DetailTypeId") + .HasColumnType("INTEGER"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ResourceId") + .HasColumnType("TEXT"); + + b.Property("UpdatedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DetailTypeId"); + + b.ToTable("Intensities"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.Detail", b => + { + b.HasOne("EventJournal.Data.Entities.UserTypes.DetailType", "DetailType") + .WithMany() + .HasForeignKey("DetailTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("EventJournal.Data.Entities.Event", "Event") + .WithMany("Details") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("EventJournal.Data.Entities.UserTypes.Intensity", "Intensity") + .WithMany() + .HasForeignKey("IntensityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DetailType"); + + b.Navigation("Event"); + + b.Navigation("Intensity"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.Event", b => + { + b.HasOne("EventJournal.Data.Entities.UserTypes.EventType", "EventType") + .WithMany() + .HasForeignKey("EventTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EventType"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.Intensity", b => + { + b.HasOne("EventJournal.Data.Entities.UserTypes.DetailType", null) + .WithMany("AllowedIntensities") + .HasForeignKey("DetailTypeId"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.Event", b => + { + b.Navigation("Details"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.DetailType", b => + { + b.Navigation("AllowedIntensities"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/EventJournal.Data/Migrations/20250819054033_InitialMigrationTry3.cs b/EventJournal.Data/Migrations/20250824035035_InitialMigration.V5.cs similarity index 92% rename from EventJournal.Data/Migrations/20250819054033_InitialMigrationTry3.cs rename to EventJournal.Data/Migrations/20250824035035_InitialMigration.V5.cs index 3a3e2ba..92d2626 100644 --- a/EventJournal.Data/Migrations/20250819054033_InitialMigrationTry3.cs +++ b/EventJournal.Data/Migrations/20250824035035_InitialMigration.V5.cs @@ -6,7 +6,7 @@ namespace EventJournal.Data.Migrations { /// - public partial class InitialMigrationTry3 : Migration + public partial class InitialMigrationV5 : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) @@ -15,17 +15,18 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "DetailTypes", columns: table => new { - DetailTypeId = table.Column(type: "INTEGER", nullable: false) + Id = table.Column(type: "INTEGER", nullable: false) .Annotation("Sqlite:Autoincrement", true), - DetailTypeResourceId = table.Column(type: "TEXT", nullable: false), + ResourceId = table.Column(type: "TEXT", nullable: false), Name = table.Column(type: "TEXT", nullable: false), Description = table.Column(type: "TEXT", maxLength: 500, nullable: true), + IntensitySortType = table.Column(type: "INTEGER", nullable: false), CreatedDate = table.Column(type: "TEXT", nullable: false), UpdatedDate = table.Column(type: "TEXT", nullable: false) }, constraints: table => { - table.PrimaryKey("PK_DetailTypes", x => x.DetailTypeId); + table.PrimaryKey("PK_DetailTypes", x => x.Id); }); migrationBuilder.CreateTable( @@ -55,8 +56,7 @@ protected override void Up(MigrationBuilder migrationBuilder) Name = table.Column(type: "TEXT", maxLength: 50, nullable: false), Level = table.Column(type: "INTEGER", nullable: false), Description = table.Column(type: "TEXT", maxLength: 500, nullable: true), - DefaultSortType = table.Column(type: "INTEGER", nullable: false), - DetailTypeId = table.Column(type: "INTEGER", nullable: false), + DetailTypeId = table.Column(type: "INTEGER", nullable: true), CreatedDate = table.Column(type: "TEXT", nullable: false), UpdatedDate = table.Column(type: "TEXT", nullable: false) }, @@ -67,8 +67,7 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "FK_Intensities_DetailTypes_DetailTypeId", column: x => x.DetailTypeId, principalTable: "DetailTypes", - principalColumn: "DetailTypeId", - onDelete: ReferentialAction.Cascade); + principalColumn: "Id"); }); migrationBuilder.CreateTable( @@ -117,7 +116,7 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "FK_Details_DetailTypes_DetailTypeId", column: x => x.DetailTypeId, principalTable: "DetailTypes", - principalColumn: "DetailTypeId", + principalColumn: "Id", onDelete: ReferentialAction.Cascade); table.ForeignKey( name: "FK_Details_Events_EventId", diff --git a/EventJournal.Data/Migrations/20250819054033_InitialMigrationTry3.Designer.cs b/EventJournal.Data/Migrations/20250824044228_fixDetailTypeIntensityRelationshipAgain.Designer.cs similarity index 95% rename from EventJournal.Data/Migrations/20250819054033_InitialMigrationTry3.Designer.cs rename to EventJournal.Data/Migrations/20250824044228_fixDetailTypeIntensityRelationshipAgain.Designer.cs index 5d6361f..55acfd7 100644 --- a/EventJournal.Data/Migrations/20250819054033_InitialMigrationTry3.Designer.cs +++ b/EventJournal.Data/Migrations/20250824044228_fixDetailTypeIntensityRelationshipAgain.Designer.cs @@ -11,8 +11,8 @@ namespace EventJournal.Data.Migrations { [DbContext(typeof(DatabaseContext))] - [Migration("20250819054033_InitialMigrationTry3")] - partial class InitialMigrationTry3 + [Migration("20250824044228_fixDetailTypeIntensityRelationshipAgain")] + partial class fixDetailTypeIntensityRelationshipAgain { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -96,7 +96,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.DetailType", b => { - b.Property("DetailTypeId") + b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); @@ -107,17 +107,20 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasMaxLength(500) .HasColumnType("TEXT"); - b.Property("DetailTypeResourceId") - .HasColumnType("TEXT"); + b.Property("IntensitySortType") + .HasColumnType("INTEGER"); b.Property("Name") .IsRequired() .HasColumnType("TEXT"); + b.Property("ResourceId") + .HasColumnType("TEXT"); + b.Property("UpdatedDate") .HasColumnType("TEXT"); - b.HasKey("DetailTypeId"); + b.HasKey("Id"); b.ToTable("DetailTypes"); }); @@ -159,9 +162,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("CreatedDate") .HasColumnType("TEXT"); - b.Property("DefaultSortType") - .HasColumnType("INTEGER"); - b.Property("Description") .HasMaxLength(500) .HasColumnType("TEXT"); @@ -230,11 +230,13 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.Intensity", b => { - b.HasOne("EventJournal.Data.Entities.UserTypes.DetailType", null) + b.HasOne("EventJournal.Data.Entities.UserTypes.DetailType", "DetailType") .WithMany("AllowedIntensities") .HasForeignKey("DetailTypeId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("DetailType"); }); modelBuilder.Entity("EventJournal.Data.Entities.Event", b => diff --git a/EventJournal.Data/Migrations/20250824044228_fixDetailTypeIntensityRelationshipAgain.cs b/EventJournal.Data/Migrations/20250824044228_fixDetailTypeIntensityRelationshipAgain.cs new file mode 100644 index 0000000..22c8884 --- /dev/null +++ b/EventJournal.Data/Migrations/20250824044228_fixDetailTypeIntensityRelationshipAgain.cs @@ -0,0 +1,59 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace EventJournal.Data.Migrations +{ + /// + public partial class fixDetailTypeIntensityRelationshipAgain : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Intensities_DetailTypes_DetailTypeId", + table: "Intensities"); + + migrationBuilder.AlterColumn( + name: "DetailTypeId", + table: "Intensities", + type: "INTEGER", + nullable: false, + defaultValue: 0, + oldClrType: typeof(int), + oldType: "INTEGER", + oldNullable: true); + + migrationBuilder.AddForeignKey( + name: "FK_Intensities_DetailTypes_DetailTypeId", + table: "Intensities", + column: "DetailTypeId", + principalTable: "DetailTypes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Intensities_DetailTypes_DetailTypeId", + table: "Intensities"); + + migrationBuilder.AlterColumn( + name: "DetailTypeId", + table: "Intensities", + type: "INTEGER", + nullable: true, + oldClrType: typeof(int), + oldType: "INTEGER"); + + migrationBuilder.AddForeignKey( + name: "FK_Intensities_DetailTypes_DetailTypeId", + table: "Intensities", + column: "DetailTypeId", + principalTable: "DetailTypes", + principalColumn: "Id"); + } + } +} diff --git a/EventJournal.Data/Migrations/DatabaseContextModelSnapshot.cs b/EventJournal.Data/Migrations/DatabaseContextModelSnapshot.cs index a7a622d..580c68e 100644 --- a/EventJournal.Data/Migrations/DatabaseContextModelSnapshot.cs +++ b/EventJournal.Data/Migrations/DatabaseContextModelSnapshot.cs @@ -93,7 +93,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.DetailType", b => { - b.Property("DetailTypeId") + b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); @@ -104,17 +104,20 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(500) .HasColumnType("TEXT"); - b.Property("DetailTypeResourceId") - .HasColumnType("TEXT"); + b.Property("IntensitySortType") + .HasColumnType("INTEGER"); b.Property("Name") .IsRequired() .HasColumnType("TEXT"); + b.Property("ResourceId") + .HasColumnType("TEXT"); + b.Property("UpdatedDate") .HasColumnType("TEXT"); - b.HasKey("DetailTypeId"); + b.HasKey("Id"); b.ToTable("DetailTypes"); }); @@ -156,9 +159,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedDate") .HasColumnType("TEXT"); - b.Property("DefaultSortType") - .HasColumnType("INTEGER"); - b.Property("Description") .HasMaxLength(500) .HasColumnType("TEXT"); @@ -227,11 +227,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.Intensity", b => { - b.HasOne("EventJournal.Data.Entities.UserTypes.DetailType", null) + b.HasOne("EventJournal.Data.Entities.UserTypes.DetailType", "DetailType") .WithMany("AllowedIntensities") .HasForeignKey("DetailTypeId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("DetailType"); }); modelBuilder.Entity("EventJournal.Data.Entities.Event", b => diff --git a/EventJournal.Data/UserTypeRepositories/DetailTypeRepository.cs b/EventJournal.Data/UserTypeRepositories/DetailTypeRepository.cs index 0c895bf..3f5e62f 100644 --- a/EventJournal.Data/UserTypeRepositories/DetailTypeRepository.cs +++ b/EventJournal.Data/UserTypeRepositories/DetailTypeRepository.cs @@ -1,6 +1,19 @@ using EventJournal.Data.Entities.UserTypes; +using Microsoft.EntityFrameworkCore; namespace EventJournal.Data.UserTypeRepositories { public class DetailTypeRepository(IDatabaseContext db) : BaseRepository(db, db.DetailTypes), IDetailTypeRepository { + public override async Task> GetAllAsync() { + return await table + .Include(dt => dt.AllowedIntensities) + .ToListAsync() + .ConfigureAwait(false); + } + + public override Task GetByResourceIdAsync(Guid resourceId) { + return table + .Include(dt => dt.AllowedIntensities) + .FirstOrDefaultAsync(dt => dt.ResourceId == resourceId); + } } } diff --git a/EventJournal.Data/UserTypeRepositories/IntensityRepository.cs b/EventJournal.Data/UserTypeRepositories/IntensityRepository.cs index ce62f8b..45aa43f 100644 --- a/EventJournal.Data/UserTypeRepositories/IntensityRepository.cs +++ b/EventJournal.Data/UserTypeRepositories/IntensityRepository.cs @@ -1,7 +1,18 @@ using EventJournal.Data.Entities.UserTypes; +using Microsoft.EntityFrameworkCore; namespace EventJournal.Data.UserTypeRepositories { public class IntensityRepository(IDatabaseContext db) : BaseRepository(db, db.Intensities), IIntensityRepository { + public override async Task> GetAllAsync() { + return await table + .OrderBy(i => i.Level) + .ToListAsync() + .ConfigureAwait(false); + } + public override Task GetByResourceIdAsync(Guid resourceId) { + return table + .FirstOrDefaultAsync(i => i.ResourceId == resourceId); + } } } diff --git a/EventJournal.DomainModels/BaseDto.cs b/EventJournal.DomainModels/BaseDto.cs index f91db33..6409adc 100644 --- a/EventJournal.DomainModels/BaseDto.cs +++ b/EventJournal.DomainModels/BaseDto.cs @@ -8,7 +8,7 @@ public abstract class BaseDto { public DateTime UpdatedDate { get; set; } = DateTime.UtcNow; [JsonIgnore] - public Guid ResourceId { get; set; } + public virtual Guid ResourceId { get; set; } /// /// This method is predominantly for updating an entity based on the values in another entity. diff --git a/EventJournal.DomainModels/DetailDto.cs b/EventJournal.DomainModels/DetailDto.cs index 5425241..2f4a7e9 100644 --- a/EventJournal.DomainModels/DetailDto.cs +++ b/EventJournal.DomainModels/DetailDto.cs @@ -3,7 +3,10 @@ namespace EventJournal.DomainDto { public class DetailDto : BaseDto { - public Guid DetailResourceId { get { return ResourceId; } set { ResourceId = value; } } + public override Guid ResourceId { get; set; } + + [Required] + public required EventDto EventDto { get; set; } [Required] public virtual required DetailTypeDto DetailType { get; set; } @@ -14,15 +17,13 @@ public class DetailDto : BaseDto { [MaxLength(512)] public string? Notes { get; set; } - public static DetailDto DefaultDetailDto() { - var detailType = DetailTypeDto.DefaultDetailTypeDto(); - return new DetailDto { - DetailResourceId = Guid.Parse("00000000-0000-0000-0000-000000000001"), - DetailType = detailType, - Intensity = IntensityDto.DefaultIntensityDto(detailType.ResourceId), - Notes = "Notes\nmore notes" - }; - } + public static readonly DetailDto DefaultDetailDto = new() { + ResourceId = Guid.Parse("00000000-0000-0000-0000-000000000001"), + EventDto = EventDto.DefaultEventDto, + DetailType = DetailTypeDto.DefaultDetailTypeDto, + Intensity = DetailTypeDto.DefaultDetailTypeDto.AllowedIntensities.First(), + Notes = "Congratulations on starting your Event History!\nTake the next step and add another event!" + }; internal override void CopyUserValues(T source) { var sourceDetail = source as DetailDto ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(DetailDto)}"); @@ -30,5 +31,8 @@ internal override void CopyUserValues(T source) { Intensity = sourceDetail.Intensity; Notes = sourceDetail.Notes; } + public override string ToString() { + return this.Serialize(); + } } } diff --git a/EventJournal.DomainModels/DtoHelper.cs b/EventJournal.DomainModels/DtoHelper.cs index fdc5c64..e179ad0 100644 --- a/EventJournal.DomainModels/DtoHelper.cs +++ b/EventJournal.DomainModels/DtoHelper.cs @@ -1,42 +1,38 @@ -using EventJournal.DomainDto.UserTypes; +using System.Runtime.CompilerServices; using System.Text.Json; namespace EventJournal.DomainDto { public static partial class DtoHelper { - public static T UpdateEntity(this T destination, T source) where T : BaseDto { + //TODO: consider using AutoMapper for this if it is even needed + public static T UpdateDTO(this T destination, T source) where T : BaseDto { + ArgumentNullException.ThrowIfNull(destination); + ArgumentNullException.ThrowIfNull(source); + if (destination.ResourceId != source.ResourceId) + //TODO: custom exception that supports a ThrowIf parameter? + throw new InvalidOperationException("ResourceIds do not match"); destination.CopyUserValues(source); destination.CreatedDate = source.CreatedDate; destination.UpdatedDate = DateTime.UtcNow; return destination; } - public static string Serialize(this T entity) where T : BaseDto { - //TODO: use reflection to find objects that inherit from BaseDto and their type - if (entity is DetailDto detail && detail != null) { - return JsonSerializer.Serialize(detail); - } - if (entity is EventDto @event && @event != null) { - return JsonSerializer.Serialize(@event); - } - if (entity is DetailTypeDto detailType && detailType != null) { - return JsonSerializer.Serialize(detailType); - } - if (entity is EventTypeDto eventTypeDto && eventTypeDto != null) { - return JsonSerializer.Serialize(eventTypeDto); - } - if (entity is IntensityDto intensityDto && intensityDto != null) { - return JsonSerializer.Serialize(intensityDto); - } - return JsonSerializer.Serialize(entity); + public static string Serialize(this T entity, JsonSerializerOptions? serializerOptions = null) where T : BaseDto { + return JsonSerializer.Serialize(entity, entity.GetType(), serializerOptions ?? DefaultSerializerOptions); } + public static string Serialize(this IEnumerable list, JsonSerializerOptions? serializerOptions = null) where T : BaseDto { + return JsonSerializer.Serialize(list, list.GetType(), serializerOptions ?? DefaultSerializerOptions); + } + + //TODO: options should be setup in bootstrap + public static readonly JsonSerializerOptions DefaultSerializerOptions = new() { + WriteIndented = true + }; + public static T? Deserialize(this string json) where T : BaseDto { //TODO: does this work properly for inherited types? return JsonSerializer.Deserialize(json); } - public static string ToString(this T entity) where T : BaseDto { - return entity.Serialize(); - } } } diff --git a/EventJournal.DomainModels/Enumerations/SortType.cs b/EventJournal.DomainModels/Enumerations/SortType.cs index 2758382..ac47543 100644 --- a/EventJournal.DomainModels/Enumerations/SortType.cs +++ b/EventJournal.DomainModels/Enumerations/SortType.cs @@ -1,8 +1,9 @@ namespace EventJournal.DomainDto.Enumerations { //TODO: why duplicate this enum? (other is in .Data.Entities.Enumerations) public enum SortType { - Custom, + None, Ascending, - Descending + Descending, + Custom } } diff --git a/EventJournal.DomainModels/EventDto.cs b/EventJournal.DomainModels/EventDto.cs index d82b477..97ba9d0 100644 --- a/EventJournal.DomainModels/EventDto.cs +++ b/EventJournal.DomainModels/EventDto.cs @@ -3,10 +3,10 @@ namespace EventJournal.DomainDto { public class EventDto : BaseDto { - public Guid EventResourceId { get { return ResourceId; } set { ResourceId = value; } } + public override Guid ResourceId { get; set; } [Required] - public EventTypeDto EventType { get; set; } = EventTypeDto.DefaultEventTypeDto(); + public required EventTypeDto EventType { get; set; } [Required] public DateTime StartTime { get; set; } = DateTime.Now; @@ -18,14 +18,13 @@ public class EventDto : BaseDto { public IEnumerable Details { get; set; } = []; - public static EventDto DefaultEventDto() { - return new EventDto { - EventResourceId = Guid.Parse("00000000-0000-0000-0000-000000000001"), - StartTime = DateTime.Now, - EndTime = DateTime.Now.AddMinutes(1).AddSeconds(1), - Description = "Description" - }; - } + public static readonly EventDto DefaultEventDto = new() { + ResourceId = Guid.Parse("00000000-0000-0000-0000-000000000001"), + StartTime = DateTime.Now, + Description = "Event History Started", + EventType = EventTypeDto.DefaultEventTypeDtos.First(), + Details = [DetailDto.DefaultDetailDto] + }; internal override void CopyUserValues(T source) { var soruceEvent = source as EventDto ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(EventDto)}"); @@ -35,5 +34,9 @@ internal override void CopyUserValues(T source) { Description = soruceEvent.Description; Details = soruceEvent.Details; } + + public override string ToString() { + return this.Serialize(); + } } } diff --git a/EventJournal.DomainModels/UserTypes/DetailTypeDto.cs b/EventJournal.DomainModels/UserTypes/DetailTypeDto.cs index a7401d0..0994086 100644 --- a/EventJournal.DomainModels/UserTypes/DetailTypeDto.cs +++ b/EventJournal.DomainModels/UserTypes/DetailTypeDto.cs @@ -1,8 +1,9 @@ -using System.ComponentModel.DataAnnotations; +using EventJournal.DomainDto.Enumerations; +using System.ComponentModel.DataAnnotations; namespace EventJournal.DomainDto.UserTypes { public class DetailTypeDto : BaseDto { - public Guid DetailTypeResourceId { get { return ResourceId; } set { ResourceId = value; } } + public override Guid ResourceId { get; set; } [Required] public required string Name { get; set; } @@ -13,19 +14,28 @@ public class DetailTypeDto : BaseDto { //TODO: is this useful/needed? public IEnumerable AllowedIntensities { get; set; } = []; - //TODO: does this belong here?? - public static DetailTypeDto DefaultDetailTypeDto() { - return new DetailTypeDto { - DetailTypeResourceId = Guid.Parse("00000000-0000-0000-0000-000000000001"), - Description = "Description", - Name = "Default DTO Type" - }; - } + [Required] + public required SortType IntensitySortType { get; set; } = SortType.Descending; + + public static readonly DetailTypeDto DefaultDetailTypeDto = new() { + ResourceId = Guid.Parse("00000000-0000-0000-0000-000000000001"), + Description = "This is a generic detail type", + Name = "Generic Detail Type", + IntensitySortType = SortType.Descending, + AllowedIntensities = IntensityDto.DefaultIntensityDtos + }; internal override void CopyUserValues(T source) { var sourceDetailType = source as DetailTypeDto ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(DetailTypeDto)}"); Name = sourceDetailType.Name; Description = sourceDetailType.Description; + IntensitySortType = sourceDetailType.IntensitySortType; + //TODO: this might need to be a deep copy depending on usage + // or might need to copy by value instead of reference (eg. call inteisity.copyvaules for each item) + AllowedIntensities = sourceDetailType.AllowedIntensities; + } + public override string ToString() { + return this.Serialize(); } } } diff --git a/EventJournal.DomainModels/UserTypes/EventTypeDto.cs b/EventJournal.DomainModels/UserTypes/EventTypeDto.cs index dfd209e..9e71a6a 100644 --- a/EventJournal.DomainModels/UserTypes/EventTypeDto.cs +++ b/EventJournal.DomainModels/UserTypes/EventTypeDto.cs @@ -2,7 +2,7 @@ namespace EventJournal.DomainDto.UserTypes { public class EventTypeDto : BaseDto { - public Guid EventTypeResourceId { get { return ResourceId; } set { ResourceId = value; } } + public override Guid ResourceId { get; set; } [Required] public required string Name { get; set; } @@ -10,31 +10,28 @@ public class EventTypeDto : BaseDto { [MaxLength(500)] public string? Description { get; set; } - //TODO: does this belong here?? - public static IEnumerable DefaultEventTypes() { - return [ - DefaultEventTypeDto(), - new EventTypeDto { EventTypeResourceId = Guid.Parse("00000000-0000-0000-0000-000000000001"), Name = "Exercise" }, - new EventTypeDto { EventTypeResourceId = Guid.Parse("00000000-0000-0000-0000-000000000002"), Name = "Bathroom Visit" }, - new EventTypeDto { EventTypeResourceId = Guid.Parse("00000000-0000-0000-0000-000000000003"), Name = "Food Consumption" }, - new EventTypeDto { EventTypeResourceId = Guid.Parse("00000000-0000-0000-0000-000000000004"), Name = "WeighIn" }, - ]; - } + public static readonly EventTypeDto DefaultEventTypeDto = new() { + ResourceId = Guid.Parse("00000000-0000-0000-0000-000000000001"), + Name = "Random Event", + Description = "Use for tracking random things like onset of pain, headache, or whatever that isn't directly associated with a specific event type." + }; - //TODO: does this belong here?? maybe should ensure default event type is created in the database at startup, and then use that instead of creating a new one here. - public static EventTypeDto DefaultEventTypeDto() { - return new EventTypeDto { - EventTypeResourceId = Guid.Parse("00000000-0000-0000-0000-000000000005"), - Name = "Random Event", - Description = "Use for tracking random things like onset of pain, headache, or whatever that isn't directly associated with a specific even type." - }; - } + public static readonly EventTypeDto[] DefaultEventTypeDtos = [ + DefaultEventTypeDto, + new EventTypeDto { ResourceId = Guid.Parse("00000000-0000-0000-0000-000000000002"), Name = "Exercise" }, + new EventTypeDto { ResourceId = Guid.Parse("00000000-0000-0000-0000-000000000003"), Name = "Bathroom Visit" }, + new EventTypeDto { ResourceId = Guid.Parse("00000000-0000-0000-0000-000000000004"), Name = "Food Consumption" }, + new EventTypeDto { ResourceId = Guid.Parse("00000000-0000-0000-0000-000000000005"), Name = "Weigh In" }, + ]; internal override void CopyUserValues(T source) { var sourceEventType = source as EventTypeDto ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(EventTypeDto)}"); Name = sourceEventType.Name; Description = sourceEventType.Description; } + public override string ToString() { + return this.Serialize(); + } } } diff --git a/EventJournal.DomainModels/UserTypes/IntensityDto.cs b/EventJournal.DomainModels/UserTypes/IntensityDto.cs index be874f6..6461649 100644 --- a/EventJournal.DomainModels/UserTypes/IntensityDto.cs +++ b/EventJournal.DomainModels/UserTypes/IntensityDto.cs @@ -3,7 +3,7 @@ namespace EventJournal.DomainDto.UserTypes { public class IntensityDto : BaseDto { - public Guid IntensityResourceId { get { return ResourceId; } set { ResourceId = value; } } + public override Guid ResourceId { get; set; } [Required, MaxLength(50)] public required string Name { get; set; } @@ -14,30 +14,64 @@ public class IntensityDto : BaseDto { [MaxLength(500)] public string? Description { get; set; } - [Required] - public required SortType DefaultSortType { get; set; } + public required DetailTypeDto DetailTypeDto { get; set; } + public static readonly IntensityDto ZeroIntensityDto = new() { + ResourceId = Guid.Parse("00000000-0000-0000-0000-000000000001"), + Level = 0, + Name = "Zero Intensity", + Description = "Not intense at all. Normal.", + DetailTypeDto = DetailTypeDto.DefaultDetailTypeDto + }; - [Required] - public required Guid DetailTypeId { get; set; } - //TODO: does this belong here?? - public static IntensityDto DefaultIntensityDto(Guid detailTypeId) { - return new IntensityDto { - IntensityResourceId = Guid.Parse("00000000-0000-0000-0000-000000000001"), - DefaultSortType = SortType.Descending, - Level = 1, - Name = "Default Intensity DTO", - Description = "Description", - DetailTypeId = detailTypeId - }; - } + public static readonly IntensityDto MildIntensityDto = new() { + ResourceId = Guid.Parse("00000000-0000-0000-0000-000000000002"), + Level = 1, + Name = "Mild Intensity", + Description = "Slightly intense, but nothing special.", + DetailTypeDto = DetailTypeDto.DefaultDetailTypeDto + }; + + public static readonly IntensityDto ModerateIntensityDto = new() { + ResourceId = Guid.Parse("00000000-0000-0000-0000-000000000003"), + Level = 2, + Name = "Moderate Intensity", + Description = "Intense but not very unbearable or uncomfortable.", + DetailTypeDto = DetailTypeDto.DefaultDetailTypeDto + }; + + public static readonly IntensityDto HighIntensityDto = new() { + ResourceId = Guid.Parse("00000000-0000-0000-0000-000000000004"), + Level = 3, + Name = "High Intensity", + Description = "Intensity that is uncomfortable but still bearable.", + DetailTypeDto = DetailTypeDto.DefaultDetailTypeDto + }; + + public static readonly IntensityDto InsaneIntensityDto = new() { + ResourceId = Guid.Parse("00000000-0000-0000-0000-000000000005"), + Level = 4, + Name = "Insane Intensity", + Description = "Unbearable. Insane. No one needs to experience this.", + DetailTypeDto = DetailTypeDto.DefaultDetailTypeDto + }; + + public static readonly IntensityDto[] DefaultIntensityDtos = [ + ZeroIntensityDto, + MildIntensityDto, + ModerateIntensityDto, + HighIntensityDto, + InsaneIntensityDto + ]; internal override void CopyUserValues(T source) { var sourceIntensity = source as IntensityDto ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(IntensityDto)}"); Name = sourceIntensity.Name; Level = sourceIntensity.Level; Description = sourceIntensity.Description; - DefaultSortType = sourceIntensity.DefaultSortType; + } + public override string ToString() { + return this.Serialize(); } } } diff --git a/EventJournal.DomainService/DetailService.cs b/EventJournal.DomainService/DetailService.cs new file mode 100644 index 0000000..c206401 --- /dev/null +++ b/EventJournal.DomainService/DetailService.cs @@ -0,0 +1,123 @@ +using AutoMapper; +using EventJournal.Data; +using EventJournal.Data.Entities; +using EventJournal.Data.Entities.UserTypes; +using EventJournal.Data.UserTypeRepositories; +using EventJournal.DomainDto; +using EventJournal.DomainDto.UserTypes; + +namespace EventJournal.DomainService { + public class DetailService( + IDetailRepository detailRepository, + IDetailTypeRepository detailTypeRepository, + IIntensityRepository intensityRepository, + IMapper mapper) + : IDetailService { + + + // ======================> Details <====================== + public async Task> GetAllDetailsAsync() { + return mapper.Map>(await detailRepository.GetAllAsync().ConfigureAwait(false)); + } + + public async Task GetDetailByIdAsync(Guid resourceId) { + return mapper.Map(await detailRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false)); + } + + public async Task AddUpdateDetailAsync(DetailDto dto) { + ArgumentNullException.ThrowIfNull(dto); + //TODO: additional validations? + var entity = await detailRepository.AddUpdateAsync(mapper.Map(dto)).ConfigureAwait(false); + await detailRepository.SaveChangesAsync().ConfigureAwait(false); + return mapper.Map(entity); + } + + public async Task> AddUpdateDetailsAsync(IEnumerable dtos) { + ArgumentNullException.ThrowIfNull(dtos); + var detailEntities = await detailRepository.AddUpdateAsync(mapper.Map>(dtos)).ConfigureAwait(false); + await detailRepository.SaveChangesAsync().ConfigureAwait(false); + return mapper.Map>(detailEntities); + } + + public async Task DeleteDetailAsync(Guid resourceId) { + var entity = await detailRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false); + if (entity == null) { + return; + } + detailRepository.Delete(entity); + await detailRepository.SaveChangesAsync().ConfigureAwait(false); + } + + // ======================> Detail Types <====================== + public async Task> GetAllDetailTypesAsync() { + return mapper.Map>(await detailTypeRepository.GetAllAsync().ConfigureAwait(false)); + } + + public async Task GetDetailTypeByIdAsync(Guid resourceId) { + return mapper.Map(await detailTypeRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false)); + } + + public async Task AddUpdateDetailTypeAsync(DetailTypeDto dto) { + ArgumentNullException.ThrowIfNull(dto); + //TODO: additional validations? + var entity = await detailTypeRepository.AddUpdateAsync(mapper.Map(dto)).ConfigureAwait(false); + await detailTypeRepository.SaveChangesAsync().ConfigureAwait(false); + return mapper.Map(entity); + } + + public async Task> AddUpdateDetailTypesAsync(IEnumerable dtos) { + ArgumentNullException.ThrowIfNull(dtos); + var detailTypeEntities = await detailTypeRepository.AddUpdateAsync(mapper.Map>(dtos)).ConfigureAwait(false); + await detailTypeRepository.SaveChangesAsync().ConfigureAwait(false); + return mapper.Map>(detailTypeEntities); + } + + public async Task DeleteDetailTypeAsync(Guid resourceId) { + var entity = await detailTypeRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false); + if (entity == null) { + return; + } + detailTypeRepository.Delete(entity); + await detailTypeRepository.SaveChangesAsync().ConfigureAwait(false); + } + + // ======================> Intensities <====================== + public async Task> GetAllIntensitiesAsync() { + return mapper.Map>(await intensityRepository.GetAllAsync().ConfigureAwait(false)); + } + + public async Task GetIntensityByIdAsync(Guid resourceId) { + return mapper.Map(await intensityRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false)); + } + + public async Task AddUpdateIntensityAsync(IntensityDto dto) { + ArgumentNullException.ThrowIfNull(dto); + //TODO: additional validations? + var entity = await intensityRepository.AddUpdateAsync(mapper.Map(dto)).ConfigureAwait(false); + await intensityRepository.SaveChangesAsync().ConfigureAwait(false); + return mapper.Map(entity); + } + + public async Task> AddUpdateIntensitiesAsync(IEnumerable dtos) { + ArgumentNullException.ThrowIfNull(dtos); + var intensityEntities = await intensityRepository.AddUpdateAsync(mapper.Map>(dtos)).ConfigureAwait(false); + await intensityRepository.SaveChangesAsync().ConfigureAwait(false); + return mapper.Map>(intensityEntities); + } + + public async Task DeleteIntensityAsync(Guid resourceId) { + var entity = await intensityRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false); + if (entity == null) { + return; + } + intensityRepository.Delete(entity); + await intensityRepository.SaveChangesAsync().ConfigureAwait(false); + } + + public async Task AddTestDataAsync() { + await AddUpdateDetailTypeAsync(DetailTypeDto.DefaultDetailTypeDto).ConfigureAwait(false); + await AddUpdateIntensitiesAsync(IntensityDto.DefaultIntensityDtos).ConfigureAwait(false); + //await AddUpdateDetailAsync(DetailDto.DefaultDetailDto).ConfigureAwait(false); + } + } +} diff --git a/EventJournal.DomainService/EventService.cs b/EventJournal.DomainService/EventService.cs index 58ce2c8..e1f06ef 100644 --- a/EventJournal.DomainService/EventService.cs +++ b/EventJournal.DomainService/EventService.cs @@ -1,51 +1,89 @@ using AutoMapper; using EventJournal.Data; using EventJournal.Data.Entities; +using EventJournal.Data.Entities.UserTypes; +using EventJournal.Data.UserTypeRepositories; using EventJournal.DomainDto; -using EventJournal.DomainService.Exceptions; -using System.Linq; +using EventJournal.DomainDto.UserTypes; namespace EventJournal.DomainService { public class EventService( - IEventRepository eventRepository, - IDetailRepository detailRepository, - IMapper mapper) : IEventService { + IEventRepository eventRepository, + IEventTypeRepository eventTypeRepository, + IMapper mapper) + : IEventService { + // ======================> Events <====================== public async Task> GetAllEventsAsync() { return mapper.Map>(await eventRepository.GetAllAsync().ConfigureAwait(false)); } + public async Task GetEventByIdAsync(Guid resourceId) { return mapper.Map(await eventRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false)); } + public async Task AddUpdateEventAsync(EventDto dto) { ArgumentNullException.ThrowIfNull(dto); //TODO: additional validations? - var eventEntity = mapper.Map(dto); - eventEntity = await eventRepository.AddUpdateAsync(eventEntity).ConfigureAwait(false); - var resultDto = mapper.Map(eventEntity); - return resultDto; + var eventEntity = await eventRepository.AddUpdateAsync(mapper.Map(dto)).ConfigureAwait(false); + await eventRepository.SaveChangesAsync().ConfigureAwait(false); + return mapper.Map(eventEntity); + } + + public async Task> AddUpdateEventsAsync(IEnumerable dtos) { + ArgumentNullException.ThrowIfNull(dtos); + var eventEntities = await eventRepository.AddUpdateAsync(mapper.Map>(dtos)).ConfigureAwait(false); + await eventRepository.SaveChangesAsync().ConfigureAwait(false); + return mapper.Map>(eventEntities); + } + public async Task DeleteEventAsync(Guid resourceId) { var entity = await eventRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false); - ResourceNotFoundException.ThrowIfNull(entity, $"Event with resource id {resourceId} not found"); - await eventRepository.DeleteAsync(entity).ConfigureAwait(false); + if (entity == null) { + return; + } + eventRepository.Delete(entity); + await eventRepository.SaveChangesAsync().ConfigureAwait(false); } - public async Task> GetAllDetailsAsync() { - return mapper.Map>(await detailRepository.GetAllAsync().ConfigureAwait(false)); + // ======================> Event Types <====================== + public async Task> GetAllEventTypesAsync() { + return mapper.Map>(await eventTypeRepository.GetAllAsync().ConfigureAwait(false)); } - public async Task GetDetailByIdAsync(Guid resourceId) { - return mapper.Map(await detailRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false)); + + public async Task GetEventTypeByIdAsync(Guid resourceId) { + return mapper.Map(await eventTypeRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false)); } - public async Task AddUpdateDetailAsync(DetailDto dto) { + + public async Task AddUpdateEventTypeAsync(EventTypeDto dto) { ArgumentNullException.ThrowIfNull(dto); //TODO: additional validations? - return mapper.Map(await detailRepository.AddUpdateAsync(mapper.Map(dto)).ConfigureAwait(false)); + var eventTypeEntity = await eventTypeRepository.AddUpdateAsync(mapper.Map(dto)).ConfigureAwait(false); + await eventTypeRepository.SaveChangesAsync().ConfigureAwait(false); + return mapper.Map(eventTypeEntity); + } + + public async Task> AddUpdateEventTypesAsync(IEnumerable dtos) { + ArgumentNullException.ThrowIfNull(dtos); + var eventTypeEntities = await eventTypeRepository.AddUpdateAsync(mapper.Map>(dtos)).ConfigureAwait(false); + await eventTypeRepository.SaveChangesAsync().ConfigureAwait(false); + return mapper.Map>(eventTypeEntities); } - public async Task DeleteDetailAsync(Guid resourceId) { - var entity = await detailRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false); - ResourceNotFoundException.ThrowIfNull(entity, $"Event with resource id {resourceId} not found"); - await detailRepository.DeleteAsync(entity).ConfigureAwait(false); + + public async Task DeleteEventTypeAsync(Guid resourceId) { + var entity = await eventTypeRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false); + if (entity == null) { + return; + } + eventTypeRepository.Delete(entity); + await eventTypeRepository.SaveChangesAsync().ConfigureAwait(false); + } + + + public async Task AddTestDataAsync() { + await AddUpdateEventTypesAsync(EventTypeDto.DefaultEventTypeDtos).ConfigureAwait(false); + await AddUpdateEventAsync(EventDto.DefaultEventDto); } } } diff --git a/EventJournal.DomainService/Exceptions/ResourceNotFoundException.cs b/EventJournal.DomainService/Exceptions/ResourceNotFoundException.cs index ada6d69..01fd19f 100644 --- a/EventJournal.DomainService/Exceptions/ResourceNotFoundException.cs +++ b/EventJournal.DomainService/Exceptions/ResourceNotFoundException.cs @@ -2,6 +2,10 @@ using System.Runtime.CompilerServices; namespace EventJournal.DomainService.Exceptions { + //TODO: add logging? + //TODO: consider making this a generic NotFoundException that takes the type that was not found? + //TODO: [Serializable] ? + //TODO: is this needed? public class ResourceNotFoundException : Exception { public ResourceNotFoundException() { } diff --git a/EventJournal.DomainService/IDetailService.cs b/EventJournal.DomainService/IDetailService.cs new file mode 100644 index 0000000..03848aa --- /dev/null +++ b/EventJournal.DomainService/IDetailService.cs @@ -0,0 +1,30 @@ +using EventJournal.DomainDto; +using EventJournal.DomainDto.UserTypes; + +namespace EventJournal.DomainService { + public interface IDetailService { + + // ======================> Details <====================== + Task> GetAllDetailsAsync(); + Task GetDetailByIdAsync(Guid resourceId); + Task AddUpdateDetailAsync(DetailDto dto); + Task> AddUpdateDetailsAsync(IEnumerable dtos); + Task DeleteDetailAsync(Guid resourceId); + + // ======================> Detail Types <====================== + Task> GetAllDetailTypesAsync(); + Task GetDetailTypeByIdAsync(Guid resourceId); + Task AddUpdateDetailTypeAsync(DetailTypeDto dto); + Task> AddUpdateDetailTypesAsync(IEnumerable dtos); + Task DeleteDetailTypeAsync(Guid resourceId); + + // =======================> Intensities <====================== + Task> GetAllIntensitiesAsync(); + Task GetIntensityByIdAsync(Guid resourceId); + Task AddUpdateIntensityAsync(IntensityDto dto); + Task> AddUpdateIntensitiesAsync(IEnumerable dtos); + Task DeleteIntensityAsync(Guid resourceId); + + Task AddTestDataAsync(); + } +} \ No newline at end of file diff --git a/EventJournal.DomainService/IEventService.cs b/EventJournal.DomainService/IEventService.cs index 0e77d0d..a471e95 100644 --- a/EventJournal.DomainService/IEventService.cs +++ b/EventJournal.DomainService/IEventService.cs @@ -1,15 +1,22 @@ using EventJournal.DomainDto; +using EventJournal.DomainDto.UserTypes; namespace EventJournal.DomainService { public interface IEventService { + // ======================> Events <====================== Task> GetAllEventsAsync(); Task GetEventByIdAsync(Guid resourceId); Task AddUpdateEventAsync(EventDto dto); + Task> AddUpdateEventsAsync(IEnumerable dtos); Task DeleteEventAsync(Guid resourceId); - Task> GetAllDetailsAsync(); - Task GetDetailByIdAsync(Guid resourceId); - Task AddUpdateDetailAsync(DetailDto dto); - Task DeleteDetailAsync(Guid resourceId); + // ======================> Event Types <====================== + Task> GetAllEventTypesAsync(); + Task GetEventTypeByIdAsync(Guid resourceId); + Task AddUpdateEventTypeAsync(EventTypeDto dto); + Task> AddUpdateEventTypesAsync(IEnumerable dtos); + Task DeleteEventTypeAsync(Guid resourceId); + + Task AddTestDataAsync(); } } \ No newline at end of file diff --git a/EventJournal.DomainService/IUserTypeService.cs b/EventJournal.DomainService/IUserTypeService.cs deleted file mode 100644 index af4bed1..0000000 --- a/EventJournal.DomainService/IUserTypeService.cs +++ /dev/null @@ -1,22 +0,0 @@ -using EventJournal.DomainDto.UserTypes; - -namespace EventJournal.DomainService { - public interface IUserTypeService { - - - Task> GetAllDetailTypesAsync(); - Task GetDetailTypeByIdAsync(Guid resourceId); - Task AddUpdateDetailTypeAsync(DetailTypeDto dto); - Task DeleteDetailTypeAsync(Guid resourceId); - - Task> GetAllEventTypesAsync(); - Task GetEventTypeByIdAsync(Guid resourceId); - Task AddUpdateEventTypeAsync(EventTypeDto dto); - Task DeleteEventTypeAsync(Guid resourceId); - - Task> GetAllIntensitiesAsync(); - Task GetIntensityByIdAsync(Guid resourceId); - Task AddUpdateIntensityAsync(IntensityDto dto); - Task DeleteIntensityAsync(Guid resourceId); - } -} \ No newline at end of file diff --git a/EventJournal.DomainService/UserTypeService.cs b/EventJournal.DomainService/UserTypeService.cs deleted file mode 100644 index 51d3ab3..0000000 --- a/EventJournal.DomainService/UserTypeService.cs +++ /dev/null @@ -1,65 +0,0 @@ -using AutoMapper; -using EventJournal.Data.Entities.UserTypes; -using EventJournal.Data.UserTypeRepositories; -using EventJournal.DomainDto.UserTypes; -using EventJournal.DomainService.Exceptions; - -namespace EventJournal.DomainService { - public class UserTypeService( - IDetailTypeRepository detailRepository, - IEventTypeRepository eventTypeRepository, - IIntensityRepository intensityRepository, - IMapper mapper) : IUserTypeService { - - public async Task> GetAllDetailTypesAsync() { - return mapper.Map>(await detailRepository.GetAllAsync().ConfigureAwait(false)); - } - public async Task GetDetailTypeByIdAsync(Guid resourceId) { - return mapper.Map(await detailRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false)); - } - public async Task AddUpdateDetailTypeAsync(DetailTypeDto dto) { - ArgumentNullException.ThrowIfNull(dto); - //TODO: additional validations? - return mapper.Map(await detailRepository.AddUpdateAsync(mapper.Map(dto)).ConfigureAwait(false)); - } - public async Task DeleteDetailTypeAsync(Guid resourceId) { - var entity = await detailRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false); - ResourceNotFoundException.ThrowIfNull(entity, $"Event with resource id {resourceId} not found"); - await detailRepository.DeleteAsync(entity); - } - - public async Task> GetAllEventTypesAsync() { - return mapper.Map>(await eventTypeRepository.GetAllAsync().ConfigureAwait(false)); - } - public async Task GetEventTypeByIdAsync(Guid resourceId) { - return mapper.Map(await eventTypeRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false)); - } - public async Task AddUpdateEventTypeAsync(EventTypeDto dto) { - ArgumentNullException.ThrowIfNull(dto); - //TODO: additional validations? - return mapper.Map(await eventTypeRepository.AddUpdateAsync(mapper.Map(dto)).ConfigureAwait(false)); - } - public async Task DeleteEventTypeAsync(Guid resourceId) { - var entity = await eventTypeRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false); - ResourceNotFoundException.ThrowIfNull(entity, $"Event with resource id {resourceId} not found"); - await eventTypeRepository.DeleteAsync(entity); - } - - public async Task> GetAllIntensitiesAsync() { - return mapper.Map>(await intensityRepository.GetAllAsync().ConfigureAwait(false)); - } - public async Task GetIntensityByIdAsync(Guid resourceId) { - return mapper.Map(await intensityRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false)); - } - public async Task AddUpdateIntensityAsync(IntensityDto dto) { - ArgumentNullException.ThrowIfNull(dto); - //TODO: additional validations? - return mapper.Map(await intensityRepository.AddUpdateAsync(mapper.Map(dto)).ConfigureAwait(false)); - } - public async Task DeleteIntensityAsync(Guid resourceId) { - var entity = await intensityRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false); - ResourceNotFoundException.ThrowIfNull(entity, $"Event with resource id {resourceId} not found"); - await intensityRepository.DeleteAsync(entity); - } - } -} diff --git a/EventJournal.Models/EventDataResponseModel.cs b/EventJournal.Models/EventDataResponseModel.cs new file mode 100644 index 0000000..6996f35 --- /dev/null +++ b/EventJournal.Models/EventDataResponseModel.cs @@ -0,0 +1,10 @@ +using EventJournal.DomainDto; +using EventJournal.DomainDto.UserTypes; + +namespace EventJournal.PublicModels { + public class EventDataResponseModel { + public required IList DetailTypes { get; set; } + public required IList EventTypes { get; set; } + public required IList Events { get; set; } + } +} \ No newline at end of file diff --git a/EventJournal.Models/EventJournal.PublicModels.csproj b/EventJournal.Models/EventJournal.PublicModels.csproj new file mode 100644 index 0000000..2d94a24 --- /dev/null +++ b/EventJournal.Models/EventJournal.PublicModels.csproj @@ -0,0 +1,13 @@ + + + + net9.0 + enable + enable + + + + + + + diff --git a/EventJournal.sln b/EventJournal.sln index 614baca..739f9e1 100644 --- a/EventJournal.sln +++ b/EventJournal.sln @@ -19,6 +19,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.DomainService" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.DomainDto", "EventJournal.DomainModels\EventJournal.DomainDto.csproj", "{153C0741-E492-490C-8C90-FB5805FD0AFE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.PublicModels", "EventJournal.Models\EventJournal.PublicModels.csproj", "{2EE6003E-F3DD-471A-9DDA-8CF9A7602005}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -45,6 +47,10 @@ Global {153C0741-E492-490C-8C90-FB5805FD0AFE}.Debug|Any CPU.Build.0 = Debug|Any CPU {153C0741-E492-490C-8C90-FB5805FD0AFE}.Release|Any CPU.ActiveCfg = Release|Any CPU {153C0741-E492-490C-8C90-FB5805FD0AFE}.Release|Any CPU.Build.0 = Release|Any CPU + {2EE6003E-F3DD-471A-9DDA-8CF9A7602005}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2EE6003E-F3DD-471A-9DDA-8CF9A7602005}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2EE6003E-F3DD-471A-9DDA-8CF9A7602005}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2EE6003E-F3DD-471A-9DDA-8CF9A7602005}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/README.md b/README.md index 6fb7d60..c125bef 100644 --- a/README.md +++ b/README.md @@ -61,14 +61,20 @@ EF https://learn.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs ### NEXT STEPS -- Add details, detail types, and intensities to the initializer -- update detail, detail type repositories to return related entities - - detail should include detail type and intensity and detailtypes.AllowedIntensities - - detail type should include valid intensities + +- Fix repository structure + - move details back to event service/repo and rename detail service to types service and move event types there + - we need a separate types service because types can be added/removed independently of events + - remove detail repository and intensity repository + - only add/delete or even GET intensities via methods on detail type + - likewise only add/delete or even GET details via methods on event +- fix default data (move from DTOs to repository so that individual entities are not duplicated when added via parents) + - eg you don't get duplicate event type entity when event is saved - in other words the event type added when event is added isn't a duplicate - test full get event with details - test full get detail type with allowed intensities - add multiple default detail types to initializer (a la event types) -- add multiple default intensities to initializer (a la event types) +- fix SortType existing in multiple places AND add a string converter for it. + ### TODO - unit tests for repositories and services. Also base entity code? - Web API to call service methods From 86973e2f0e99bf0735f9a9cd3c4d00c821f7685d Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Sun, 24 Aug 2025 11:12:55 -0400 Subject: [PATCH 24/34] fix enum to string converter --- EventJournal.CLI/Program.cs | 7 +- EventJournal.Common/Enumerations/SortType.cs | 8 + .../EventJournal.Common.csproj | 9 + EventJournal.Data/DatabaseContext.cs | 14 + .../Entities/Enumerations/SortType.cs | 9 - .../Entities/UserTypes/DetailType.cs | 4 +- .../Entities/UserTypes/Intensity.cs | 2 +- EventJournal.Data/EventJournal.Data.csproj | 4 + ...0824035035_InitialMigration.V5.Designer.cs | 250 ------------------ ...fixDetailTypeIntensityRelationshipAgain.cs | 59 ----- ...250824150901_InitialMigration.Designer.cs} | 9 +- ....cs => 20250824150901_InitialMigration.cs} | 9 +- .../DatabaseContextModelSnapshot.cs | 5 +- EventJournal.DomainModels/DtoHelper.cs | 10 +- .../Enumerations/SortType.cs | 9 - EventJournal.DomainModels/EventDto.cs | 4 +- .../EventJournal.DomainDto.csproj | 4 + .../UserTypes/DetailTypeDto.cs | 4 +- .../UserTypes/IntensityDto.cs | 3 +- EventJournal.DomainService/EventService.cs | 2 +- EventJournal.sln | 6 + README.md | 1 - 22 files changed, 77 insertions(+), 355 deletions(-) create mode 100644 EventJournal.Common/Enumerations/SortType.cs create mode 100644 EventJournal.Common/EventJournal.Common.csproj delete mode 100644 EventJournal.Data/Entities/Enumerations/SortType.cs delete mode 100644 EventJournal.Data/Migrations/20250824035035_InitialMigration.V5.Designer.cs delete mode 100644 EventJournal.Data/Migrations/20250824044228_fixDetailTypeIntensityRelationshipAgain.cs rename EventJournal.Data/Migrations/{20250824044228_fixDetailTypeIntensityRelationshipAgain.Designer.cs => 20250824150901_InitialMigration.Designer.cs} (97%) rename EventJournal.Data/Migrations/{20250824035035_InitialMigration.V5.cs => 20250824150901_InitialMigration.cs} (96%) delete mode 100644 EventJournal.DomainModels/Enumerations/SortType.cs diff --git a/EventJournal.CLI/Program.cs b/EventJournal.CLI/Program.cs index 49401de..2ce11ee 100644 --- a/EventJournal.CLI/Program.cs +++ b/EventJournal.CLI/Program.cs @@ -2,6 +2,7 @@ using EventJournal.CLI; using EventJournal.Data; using EventJournal.Data.UserTypeRepositories; +using EventJournal.DomainDto; using EventJournal.DomainService; using EventJournal.PublicModels; using Microsoft.EntityFrameworkCore; @@ -127,11 +128,7 @@ static async Task DeleteAllDataAsync(IEventService eventService, IDetailService //TODO: move to shared utilities class or bootstrapper class and remove this method private static JsonSerializerOptions GetSerializerOptions() { - return new JsonSerializerOptions { - WriteIndented = true, - PropertyNameCaseInsensitive = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }; + return DtoHelper.DefaultSerializerOptions; } static async Task ViewallDataAsync(JsonSerializerOptions options, IEventService eventService, IDetailService userTypeService) { diff --git a/EventJournal.Common/Enumerations/SortType.cs b/EventJournal.Common/Enumerations/SortType.cs new file mode 100644 index 0000000..1c124a7 --- /dev/null +++ b/EventJournal.Common/Enumerations/SortType.cs @@ -0,0 +1,8 @@ +namespace EventJournal.Common.Enumerations { + public enum SortType { + None, + Ascending, + Descending, + Custom + } +} diff --git a/EventJournal.Common/EventJournal.Common.csproj b/EventJournal.Common/EventJournal.Common.csproj new file mode 100644 index 0000000..125f4c9 --- /dev/null +++ b/EventJournal.Common/EventJournal.Common.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/EventJournal.Data/DatabaseContext.cs b/EventJournal.Data/DatabaseContext.cs index 3c4be20..c25a206 100644 --- a/EventJournal.Data/DatabaseContext.cs +++ b/EventJournal.Data/DatabaseContext.cs @@ -1,6 +1,7 @@ using EventJournal.Data.Entities; using EventJournal.Data.Entities.UserTypes; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace EventJournal.Data { @@ -20,6 +21,19 @@ public DatabaseContext(DbContextOptions options) : base(options } + //protected override void ConfigureConventions(ModelConfigurationBuilder builder) { + // // Applies conversion to all enumerations + // _ = builder.Properties() + // .HaveConversion>() + // .HaveColumnType("nvarchar(50)"); + //} + protected override void OnModelCreating(ModelBuilder modelBuilder) { + modelBuilder + .Entity() + .Property(e => e.IntensitySortType) + .HasConversion(); + } + protected override void OnConfiguring(DbContextOptionsBuilder options) { if (!options.IsConfigured) { options.UseSqlite($"Data Source={GetSqliteDbPath()}"); diff --git a/EventJournal.Data/Entities/Enumerations/SortType.cs b/EventJournal.Data/Entities/Enumerations/SortType.cs deleted file mode 100644 index bd196c3..0000000 --- a/EventJournal.Data/Entities/Enumerations/SortType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace EventJournal.Data.Entities.Enumerations { - //TODO: why duplicate this enum? (other is in .DomainDto.Enumerations ) - public enum SortType { - None, - Ascending, - Descending, - Custom - } -} diff --git a/EventJournal.Data/Entities/UserTypes/DetailType.cs b/EventJournal.Data/Entities/UserTypes/DetailType.cs index 8d9ad08..cd92be5 100644 --- a/EventJournal.Data/Entities/UserTypes/DetailType.cs +++ b/EventJournal.Data/Entities/UserTypes/DetailType.cs @@ -1,5 +1,6 @@ -using EventJournal.Data.Entities.Enumerations; +using EventJournal.Common.Enumerations; using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; namespace EventJournal.Data.Entities.UserTypes { public class DetailType : BaseEntity { @@ -16,6 +17,7 @@ public class DetailType : BaseEntity { public string? Description { get; set; } [Required] + [Column(TypeName = "nvarchar(50)")] public SortType IntensitySortType { get; set; } = SortType.None; public ICollection AllowedIntensities { get; set; } = []; diff --git a/EventJournal.Data/Entities/UserTypes/Intensity.cs b/EventJournal.Data/Entities/UserTypes/Intensity.cs index 9b28528..29a9c63 100644 --- a/EventJournal.Data/Entities/UserTypes/Intensity.cs +++ b/EventJournal.Data/Entities/UserTypes/Intensity.cs @@ -24,7 +24,7 @@ internal override void CopyUserValues(T source) { Name = sourceIntensity.Name; Level = sourceIntensity.Level; Description = sourceIntensity.Description; - DetailType.CopyUserValues( sourceIntensity.DetailType); + DetailType = sourceIntensity.DetailType; } } diff --git a/EventJournal.Data/EventJournal.Data.csproj b/EventJournal.Data/EventJournal.Data.csproj index 3309d90..b07aa8c 100644 --- a/EventJournal.Data/EventJournal.Data.csproj +++ b/EventJournal.Data/EventJournal.Data.csproj @@ -22,4 +22,8 @@ + + + + diff --git a/EventJournal.Data/Migrations/20250824035035_InitialMigration.V5.Designer.cs b/EventJournal.Data/Migrations/20250824035035_InitialMigration.V5.Designer.cs deleted file mode 100644 index e8525e0..0000000 --- a/EventJournal.Data/Migrations/20250824035035_InitialMigration.V5.Designer.cs +++ /dev/null @@ -1,250 +0,0 @@ -// -using System; -using EventJournal.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace EventJournal.Data.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20250824035035_InitialMigration.V5")] - partial class InitialMigrationV5 - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.8"); - - modelBuilder.Entity("EventJournal.Data.Entities.Detail", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedDate") - .HasColumnType("TEXT"); - - b.Property("DetailTypeId") - .HasColumnType("INTEGER"); - - b.Property("EventId") - .HasColumnType("INTEGER"); - - b.Property("IntensityId") - .HasColumnType("INTEGER"); - - b.Property("Notes") - .HasMaxLength(512) - .HasColumnType("TEXT"); - - b.Property("ResourceId") - .HasColumnType("TEXT"); - - b.Property("UpdatedDate") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("DetailTypeId"); - - b.HasIndex("EventId"); - - b.HasIndex("IntensityId"); - - b.ToTable("Details"); - }); - - modelBuilder.Entity("EventJournal.Data.Entities.Event", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedDate") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasMaxLength(500) - .HasColumnType("TEXT"); - - b.Property("EndTime") - .HasColumnType("TEXT"); - - b.Property("EventTypeId") - .HasColumnType("INTEGER"); - - b.Property("ResourceId") - .HasColumnType("TEXT"); - - b.Property("StartTime") - .HasColumnType("TEXT"); - - b.Property("UpdatedDate") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("EventTypeId"); - - b.ToTable("Events"); - }); - - modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.DetailType", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedDate") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasMaxLength(500) - .HasColumnType("TEXT"); - - b.Property("IntensitySortType") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("ResourceId") - .HasColumnType("TEXT"); - - b.Property("UpdatedDate") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("DetailTypes"); - }); - - modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.EventType", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedDate") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasMaxLength(500) - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("ResourceId") - .HasColumnType("TEXT"); - - b.Property("UpdatedDate") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("EventTypes"); - }); - - modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.Intensity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedDate") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasMaxLength(500) - .HasColumnType("TEXT"); - - b.Property("DetailTypeId") - .HasColumnType("INTEGER"); - - b.Property("Level") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("ResourceId") - .HasColumnType("TEXT"); - - b.Property("UpdatedDate") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("DetailTypeId"); - - b.ToTable("Intensities"); - }); - - modelBuilder.Entity("EventJournal.Data.Entities.Detail", b => - { - b.HasOne("EventJournal.Data.Entities.UserTypes.DetailType", "DetailType") - .WithMany() - .HasForeignKey("DetailTypeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("EventJournal.Data.Entities.Event", "Event") - .WithMany("Details") - .HasForeignKey("EventId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("EventJournal.Data.Entities.UserTypes.Intensity", "Intensity") - .WithMany() - .HasForeignKey("IntensityId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("DetailType"); - - b.Navigation("Event"); - - b.Navigation("Intensity"); - }); - - modelBuilder.Entity("EventJournal.Data.Entities.Event", b => - { - b.HasOne("EventJournal.Data.Entities.UserTypes.EventType", "EventType") - .WithMany() - .HasForeignKey("EventTypeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("EventType"); - }); - - modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.Intensity", b => - { - b.HasOne("EventJournal.Data.Entities.UserTypes.DetailType", null) - .WithMany("AllowedIntensities") - .HasForeignKey("DetailTypeId"); - }); - - modelBuilder.Entity("EventJournal.Data.Entities.Event", b => - { - b.Navigation("Details"); - }); - - modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.DetailType", b => - { - b.Navigation("AllowedIntensities"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/EventJournal.Data/Migrations/20250824044228_fixDetailTypeIntensityRelationshipAgain.cs b/EventJournal.Data/Migrations/20250824044228_fixDetailTypeIntensityRelationshipAgain.cs deleted file mode 100644 index 22c8884..0000000 --- a/EventJournal.Data/Migrations/20250824044228_fixDetailTypeIntensityRelationshipAgain.cs +++ /dev/null @@ -1,59 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace EventJournal.Data.Migrations -{ - /// - public partial class fixDetailTypeIntensityRelationshipAgain : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_Intensities_DetailTypes_DetailTypeId", - table: "Intensities"); - - migrationBuilder.AlterColumn( - name: "DetailTypeId", - table: "Intensities", - type: "INTEGER", - nullable: false, - defaultValue: 0, - oldClrType: typeof(int), - oldType: "INTEGER", - oldNullable: true); - - migrationBuilder.AddForeignKey( - name: "FK_Intensities_DetailTypes_DetailTypeId", - table: "Intensities", - column: "DetailTypeId", - principalTable: "DetailTypes", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_Intensities_DetailTypes_DetailTypeId", - table: "Intensities"); - - migrationBuilder.AlterColumn( - name: "DetailTypeId", - table: "Intensities", - type: "INTEGER", - nullable: true, - oldClrType: typeof(int), - oldType: "INTEGER"); - - migrationBuilder.AddForeignKey( - name: "FK_Intensities_DetailTypes_DetailTypeId", - table: "Intensities", - column: "DetailTypeId", - principalTable: "DetailTypes", - principalColumn: "Id"); - } - } -} diff --git a/EventJournal.Data/Migrations/20250824044228_fixDetailTypeIntensityRelationshipAgain.Designer.cs b/EventJournal.Data/Migrations/20250824150901_InitialMigration.Designer.cs similarity index 97% rename from EventJournal.Data/Migrations/20250824044228_fixDetailTypeIntensityRelationshipAgain.Designer.cs rename to EventJournal.Data/Migrations/20250824150901_InitialMigration.Designer.cs index 55acfd7..5b7d2b8 100644 --- a/EventJournal.Data/Migrations/20250824044228_fixDetailTypeIntensityRelationshipAgain.Designer.cs +++ b/EventJournal.Data/Migrations/20250824150901_InitialMigration.Designer.cs @@ -11,8 +11,8 @@ namespace EventJournal.Data.Migrations { [DbContext(typeof(DatabaseContext))] - [Migration("20250824044228_fixDetailTypeIntensityRelationshipAgain")] - partial class fixDetailTypeIntensityRelationshipAgain + [Migration("20250824150901_InitialMigration")] + partial class InitialMigration { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -107,8 +107,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasMaxLength(500) .HasColumnType("TEXT"); - b.Property("IntensitySortType") - .HasColumnType("INTEGER"); + b.Property("IntensitySortType") + .IsRequired() + .HasColumnType("nvarchar(32)"); b.Property("Name") .IsRequired() diff --git a/EventJournal.Data/Migrations/20250824035035_InitialMigration.V5.cs b/EventJournal.Data/Migrations/20250824150901_InitialMigration.cs similarity index 96% rename from EventJournal.Data/Migrations/20250824035035_InitialMigration.V5.cs rename to EventJournal.Data/Migrations/20250824150901_InitialMigration.cs index 92d2626..e7e1488 100644 --- a/EventJournal.Data/Migrations/20250824035035_InitialMigration.V5.cs +++ b/EventJournal.Data/Migrations/20250824150901_InitialMigration.cs @@ -6,7 +6,7 @@ namespace EventJournal.Data.Migrations { /// - public partial class InitialMigrationV5 : Migration + public partial class InitialMigration : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) @@ -20,7 +20,7 @@ protected override void Up(MigrationBuilder migrationBuilder) ResourceId = table.Column(type: "TEXT", nullable: false), Name = table.Column(type: "TEXT", nullable: false), Description = table.Column(type: "TEXT", maxLength: 500, nullable: true), - IntensitySortType = table.Column(type: "INTEGER", nullable: false), + IntensitySortType = table.Column(type: "nvarchar(32)", nullable: false), CreatedDate = table.Column(type: "TEXT", nullable: false), UpdatedDate = table.Column(type: "TEXT", nullable: false) }, @@ -56,7 +56,7 @@ protected override void Up(MigrationBuilder migrationBuilder) Name = table.Column(type: "TEXT", maxLength: 50, nullable: false), Level = table.Column(type: "INTEGER", nullable: false), Description = table.Column(type: "TEXT", maxLength: 500, nullable: true), - DetailTypeId = table.Column(type: "INTEGER", nullable: true), + DetailTypeId = table.Column(type: "INTEGER", nullable: false), CreatedDate = table.Column(type: "TEXT", nullable: false), UpdatedDate = table.Column(type: "TEXT", nullable: false) }, @@ -67,7 +67,8 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "FK_Intensities_DetailTypes_DetailTypeId", column: x => x.DetailTypeId, principalTable: "DetailTypes", - principalColumn: "Id"); + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); }); migrationBuilder.CreateTable( diff --git a/EventJournal.Data/Migrations/DatabaseContextModelSnapshot.cs b/EventJournal.Data/Migrations/DatabaseContextModelSnapshot.cs index 580c68e..1c69137 100644 --- a/EventJournal.Data/Migrations/DatabaseContextModelSnapshot.cs +++ b/EventJournal.Data/Migrations/DatabaseContextModelSnapshot.cs @@ -104,8 +104,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(500) .HasColumnType("TEXT"); - b.Property("IntensitySortType") - .HasColumnType("INTEGER"); + b.Property("IntensitySortType") + .IsRequired() + .HasColumnType("nvarchar(32)"); b.Property("Name") .IsRequired() diff --git a/EventJournal.DomainModels/DtoHelper.cs b/EventJournal.DomainModels/DtoHelper.cs index e179ad0..cdc369c 100644 --- a/EventJournal.DomainModels/DtoHelper.cs +++ b/EventJournal.DomainModels/DtoHelper.cs @@ -1,5 +1,5 @@ -using System.Runtime.CompilerServices; -using System.Text.Json; +using System.Text.Json; +using System.Text.Json.Serialization; namespace EventJournal.DomainDto { public static partial class DtoHelper { @@ -26,7 +26,11 @@ public static string Serialize(this IEnumerable list, JsonSerializerOption //TODO: options should be setup in bootstrap public static readonly JsonSerializerOptions DefaultSerializerOptions = new() { - WriteIndented = true + WriteIndented = true, + PropertyNameCaseInsensitive = true, + //TODO: do we want this? + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = { new JsonStringEnumConverter() } }; public static T? Deserialize(this string json) where T : BaseDto { diff --git a/EventJournal.DomainModels/Enumerations/SortType.cs b/EventJournal.DomainModels/Enumerations/SortType.cs deleted file mode 100644 index ac47543..0000000 --- a/EventJournal.DomainModels/Enumerations/SortType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace EventJournal.DomainDto.Enumerations { - //TODO: why duplicate this enum? (other is in .Data.Entities.Enumerations) - public enum SortType { - None, - Ascending, - Descending, - Custom - } -} diff --git a/EventJournal.DomainModels/EventDto.cs b/EventJournal.DomainModels/EventDto.cs index 97ba9d0..409eacb 100644 --- a/EventJournal.DomainModels/EventDto.cs +++ b/EventJournal.DomainModels/EventDto.cs @@ -3,7 +3,7 @@ namespace EventJournal.DomainDto { public class EventDto : BaseDto { - public override Guid ResourceId { get; set; } + public override Guid ResourceId { get; set; } [Required] public required EventTypeDto EventType { get; set; } @@ -23,7 +23,7 @@ public class EventDto : BaseDto { StartTime = DateTime.Now, Description = "Event History Started", EventType = EventTypeDto.DefaultEventTypeDtos.First(), - Details = [DetailDto.DefaultDetailDto] + //Details = [DetailDto.DefaultDetailDto] }; internal override void CopyUserValues(T source) { diff --git a/EventJournal.DomainModels/EventJournal.DomainDto.csproj b/EventJournal.DomainModels/EventJournal.DomainDto.csproj index 9bc16ea..a12a7a8 100644 --- a/EventJournal.DomainModels/EventJournal.DomainDto.csproj +++ b/EventJournal.DomainModels/EventJournal.DomainDto.csproj @@ -17,4 +17,8 @@ + + + + diff --git a/EventJournal.DomainModels/UserTypes/DetailTypeDto.cs b/EventJournal.DomainModels/UserTypes/DetailTypeDto.cs index 0994086..e174f8c 100644 --- a/EventJournal.DomainModels/UserTypes/DetailTypeDto.cs +++ b/EventJournal.DomainModels/UserTypes/DetailTypeDto.cs @@ -1,4 +1,4 @@ -using EventJournal.DomainDto.Enumerations; +using EventJournal.Common.Enumerations; using System.ComponentModel.DataAnnotations; namespace EventJournal.DomainDto.UserTypes { @@ -18,7 +18,7 @@ public class DetailTypeDto : BaseDto { public required SortType IntensitySortType { get; set; } = SortType.Descending; public static readonly DetailTypeDto DefaultDetailTypeDto = new() { - ResourceId = Guid.Parse("00000000-0000-0000-0000-000000000001"), + ResourceId = Guid.Parse("00000000-0000-0000-0000-000000000001"), Description = "This is a generic detail type", Name = "Generic Detail Type", IntensitySortType = SortType.Descending, diff --git a/EventJournal.DomainModels/UserTypes/IntensityDto.cs b/EventJournal.DomainModels/UserTypes/IntensityDto.cs index 6461649..991f1f0 100644 --- a/EventJournal.DomainModels/UserTypes/IntensityDto.cs +++ b/EventJournal.DomainModels/UserTypes/IntensityDto.cs @@ -1,5 +1,4 @@ -using EventJournal.DomainDto.Enumerations; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace EventJournal.DomainDto.UserTypes { public class IntensityDto : BaseDto { diff --git a/EventJournal.DomainService/EventService.cs b/EventJournal.DomainService/EventService.cs index e1f06ef..1f620b1 100644 --- a/EventJournal.DomainService/EventService.cs +++ b/EventJournal.DomainService/EventService.cs @@ -8,7 +8,7 @@ namespace EventJournal.DomainService { public class EventService( - IEventRepository eventRepository, + IEventRepository eventRepository, IEventTypeRepository eventTypeRepository, IMapper mapper) : IEventService { diff --git a/EventJournal.sln b/EventJournal.sln index 739f9e1..159982f 100644 --- a/EventJournal.sln +++ b/EventJournal.sln @@ -21,6 +21,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.DomainDto", "E EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.PublicModels", "EventJournal.Models\EventJournal.PublicModels.csproj", "{2EE6003E-F3DD-471A-9DDA-8CF9A7602005}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.Common", "EventJournal.Common\EventJournal.Common.csproj", "{1F36B474-30D4-4170-871A-76EEB7851439}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -51,6 +53,10 @@ Global {2EE6003E-F3DD-471A-9DDA-8CF9A7602005}.Debug|Any CPU.Build.0 = Debug|Any CPU {2EE6003E-F3DD-471A-9DDA-8CF9A7602005}.Release|Any CPU.ActiveCfg = Release|Any CPU {2EE6003E-F3DD-471A-9DDA-8CF9A7602005}.Release|Any CPU.Build.0 = Release|Any CPU + {1F36B474-30D4-4170-871A-76EEB7851439}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1F36B474-30D4-4170-871A-76EEB7851439}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F36B474-30D4-4170-871A-76EEB7851439}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1F36B474-30D4-4170-871A-76EEB7851439}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/README.md b/README.md index c125bef..6343751 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,6 @@ EF https://learn.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs - test full get event with details - test full get detail type with allowed intensities - add multiple default detail types to initializer (a la event types) -- fix SortType existing in multiple places AND add a string converter for it. ### TODO - unit tests for repositories and services. Also base entity code? From 38f7d803d3f31534599dfe919ce6b321e33c49a3 Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Sun, 31 Aug 2025 17:20:32 -0400 Subject: [PATCH 25/34] WIP restructure entities, repos, and services to make dependant entities only accessable via owning entities --- EventJournal.CLI/Program.cs | 23 +- EventJournal.Data/BaseRepository.cs | 4 +- EventJournal.Data/DetailRepository.cs | 22 -- EventJournal.Data/Entities/Event.cs | 35 ++- EventJournal.Data/IDetailRepository.cs | 6 - ...0831211755_restructureEntities.Designer.cs | 255 ++++++++++++++++++ .../20250831211755_restructureEntities.cs | 34 +++ .../DatabaseContextModelSnapshot.cs | 2 +- EventJournal.DomainModels/DetailDto.cs | 11 - EventJournal.DomainModels/EventDto.cs | 8 - .../DomainMapperProfile.cs | 1 + EventJournal.DomainService/EventService.cs | 94 ++++--- EventJournal.DomainService/IEventService.cs | 17 +- ...IDetailService.cs => IUserTypesService.cs} | 15 +- .../{DetailService.cs => UserTypesService.cs} | 42 +-- README.md | 8 +- 16 files changed, 434 insertions(+), 143 deletions(-) delete mode 100644 EventJournal.Data/DetailRepository.cs delete mode 100644 EventJournal.Data/IDetailRepository.cs create mode 100644 EventJournal.Data/Migrations/20250831211755_restructureEntities.Designer.cs create mode 100644 EventJournal.Data/Migrations/20250831211755_restructureEntities.cs rename EventJournal.DomainService/{IDetailService.cs => IUserTypesService.cs} (68%) rename EventJournal.DomainService/{DetailService.cs => UserTypesService.cs} (72%) diff --git a/EventJournal.CLI/Program.cs b/EventJournal.CLI/Program.cs index 2ce11ee..896e3b8 100644 --- a/EventJournal.CLI/Program.cs +++ b/EventJournal.CLI/Program.cs @@ -14,7 +14,7 @@ private static async Task Main(string[] args) { var services = CreateServiceCollection(); var eventService = services.GetService() ?? throw new Exception("Unable to locate a valid Product Logic module"); - var userTypeService = services.GetService() ?? throw new Exception("Unable to locate a valid Order Logic module"); + var userTypeService = services.GetService() ?? throw new Exception("Unable to locate a valid Order Logic module"); bool userIsDone = false; while (!userIsDone) { //Console.WriteLine("Type '1' to "); @@ -83,12 +83,11 @@ static IServiceProvider CreateServiceCollection() { }) .AddAutoMapper(cfg => { }, typeof(DomainMapperProfile)) .AddSingleton() - .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() + .AddSingleton() .AddLogging(options => { options.AddDebug(); options.SetMinimumLevel(LogLevel.Error); @@ -101,23 +100,21 @@ static IServiceProvider CreateServiceCollection() { return servicecollection.BuildServiceProvider(); } - static async Task AddTestDataAsync(IEventService eventService, IDetailService userTypeService) { + static async Task AddTestDataAsync(IEventService eventService, IUserTypesService userTypeService) { Console.WriteLine("Adding/Resetting test data."); - await eventService.AddTestDataAsync().ConfigureAwait(false); await userTypeService.AddTestDataAsync().ConfigureAwait(false); + await eventService.AddResetTestDataAsync().ConfigureAwait(false); } - static async Task DeleteAllDataAsync(IEventService eventService, IDetailService userTypeService) { + static async Task DeleteAllDataAsync(IEventService eventService, IUserTypesService userTypeService) { Console.WriteLine("Deleting all data."); foreach (var e in await eventService.GetAllEventsAsync().ConfigureAwait(false)) { await eventService.DeleteEventAsync(e.ResourceId).ConfigureAwait(false); } - foreach (var e in await eventService.GetAllEventTypesAsync().ConfigureAwait(false)) { - await eventService.DeleteEventTypeAsync(e.ResourceId).ConfigureAwait(false); - } - foreach (var e in await userTypeService.GetAllDetailsAsync().ConfigureAwait(false)) { - await userTypeService.DeleteDetailAsync(e.ResourceId).ConfigureAwait(false); + foreach (var e in await userTypeService.GetAllEventTypesAsync().ConfigureAwait(false)) { + await userTypeService.DeleteEventTypeAsync(e.ResourceId).ConfigureAwait(false); } + foreach (var e in await userTypeService.GetAllIntensitiesAsync().ConfigureAwait(false)) { await userTypeService.DeleteIntensityAsync(e.ResourceId).ConfigureAwait(false); } @@ -131,10 +128,10 @@ private static JsonSerializerOptions GetSerializerOptions() { return DtoHelper.DefaultSerializerOptions; } - static async Task ViewallDataAsync(JsonSerializerOptions options, IEventService eventService, IDetailService userTypeService) { + static async Task ViewallDataAsync(JsonSerializerOptions options, IEventService eventService, IUserTypesService userTypeService) { EventDataResponseModel eventData = new() { DetailTypes = await userTypeService.GetAllDetailTypesAsync().ConfigureAwait(false), - EventTypes = await eventService.GetAllEventTypesAsync().ConfigureAwait(false), + EventTypes = await userTypeService.GetAllEventTypesAsync().ConfigureAwait(false), Events = await eventService.GetAllEventsAsync().ConfigureAwait(false) }; diff --git a/EventJournal.Data/BaseRepository.cs b/EventJournal.Data/BaseRepository.cs index a6b9515..40bd390 100644 --- a/EventJournal.Data/BaseRepository.cs +++ b/EventJournal.Data/BaseRepository.cs @@ -27,7 +27,7 @@ public virtual void Delete(T entity) { } public virtual async Task AddUpdateAsync(T source) { - ArgumentException.ThrowIfNullOrEmpty(nameof(source)); + ArgumentNullException.ThrowIfNull(source, nameof(source)); var entity = await GetByIdAsync(source.Id) ?? await GetByResourceIdAsync(source.ResourceId).ConfigureAwait(false); if (entity == null) { entity = await AddAsync(source).ConfigureAwait(false); @@ -38,7 +38,7 @@ public virtual async Task AddUpdateAsync(T source) { } public virtual async Task> AddUpdateAsync(IEnumerable sources) { - ArgumentException.ThrowIfNullOrEmpty(nameof(sources)); + ArgumentNullException.ThrowIfNull(sources, nameof(sources)); var result = new List(); foreach (var source in sources) { var updatedEntity = await AddUpdateAsync(source).ConfigureAwait(false); diff --git a/EventJournal.Data/DetailRepository.cs b/EventJournal.Data/DetailRepository.cs deleted file mode 100644 index 824ad14..0000000 --- a/EventJournal.Data/DetailRepository.cs +++ /dev/null @@ -1,22 +0,0 @@ -using EventJournal.Data.Entities; -using Microsoft.EntityFrameworkCore; - -namespace EventJournal.Data { - public class DetailRepository(IDatabaseContext db) : BaseRepository(db, db.Details), IDetailRepository { - public override async Task> GetAllAsync() { - return await table - .Include(d => d.DetailType) - .ThenInclude(dt => dt.AllowedIntensities) - .Include(d => d.Intensity) - .ToListAsync() - .ConfigureAwait(false); - } - public override Task GetByResourceIdAsync(Guid resourceId) { - return table - .Include(d => d.DetailType) - .ThenInclude(dt => dt.AllowedIntensities) - .Include(d => d.Intensity) - .FirstOrDefaultAsync(d => d.ResourceId == resourceId); - } - } -} diff --git a/EventJournal.Data/Entities/Event.cs b/EventJournal.Data/Entities/Event.cs index f501451..ff264d9 100644 --- a/EventJournal.Data/Entities/Event.cs +++ b/EventJournal.Data/Entities/Event.cs @@ -19,7 +19,38 @@ public class Event : BaseEntity { [MaxLength(500)] public string? Description { get; set; } - public ICollection Details { get; set; } = []; + public IReadOnlyCollection Details=> (IReadOnlyCollection)_details; + private IList _details = []; + + public Detail AddUpdateDetail(Detail detail) { + ArgumentNullException.ThrowIfNull(detail, nameof(detail)); + var existingDetail = _details.FirstOrDefault(d => d.Id == detail.Id || d.ResourceId == detail.ResourceId); + if (existingDetail == null) { + detail.Event = this; + _details.Add(detail); + return detail; + } + existingDetail.Event = this; + return existingDetail.UpdateEntity(detail); + } + + public void AddUpdateDetails(IEnumerable details) { + ArgumentNullException.ThrowIfNull(details, nameof(details)); + foreach (var detail in details) { + AddUpdateDetail(detail); + } + } + + public void RemoveDetail(Guid detailResourceId) { + var existingDetail = _details.FirstOrDefault(d => d.ResourceId == detailResourceId); + if (existingDetail != null) { + _details.Remove(existingDetail); + } + } + + public void RemoveAllDetails() { + _details.Clear(); + } internal override void CopyUserValues(T source) { var soruceEvent = source as Event ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(Event)}"); @@ -27,7 +58,7 @@ internal override void CopyUserValues(T source) { StartTime = soruceEvent.StartTime; EndTime = soruceEvent.EndTime; Description = soruceEvent.Description; - Details = soruceEvent.Details; + _details = soruceEvent._details; } } } \ No newline at end of file diff --git a/EventJournal.Data/IDetailRepository.cs b/EventJournal.Data/IDetailRepository.cs deleted file mode 100644 index 62b91a4..0000000 --- a/EventJournal.Data/IDetailRepository.cs +++ /dev/null @@ -1,6 +0,0 @@ -using EventJournal.Data.Entities; - -namespace EventJournal.Data { - public interface IDetailRepository : IBaseRepository { - } -} \ No newline at end of file diff --git a/EventJournal.Data/Migrations/20250831211755_restructureEntities.Designer.cs b/EventJournal.Data/Migrations/20250831211755_restructureEntities.Designer.cs new file mode 100644 index 0000000..8a00dca --- /dev/null +++ b/EventJournal.Data/Migrations/20250831211755_restructureEntities.Designer.cs @@ -0,0 +1,255 @@ +// +using System; +using EventJournal.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace EventJournal.Data.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20250831211755_restructureEntities")] + partial class restructureEntities + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.8"); + + modelBuilder.Entity("EventJournal.Data.Entities.Detail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("DetailTypeId") + .HasColumnType("INTEGER"); + + b.Property("EventId") + .HasColumnType("INTEGER"); + + b.Property("IntensityId") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("ResourceId") + .HasColumnType("TEXT"); + + b.Property("UpdatedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DetailTypeId"); + + b.HasIndex("EventId"); + + b.HasIndex("IntensityId"); + + b.ToTable("Details"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("EndTime") + .HasColumnType("TEXT"); + + b.Property("EventTypeId") + .HasColumnType("INTEGER"); + + b.Property("ResourceId") + .HasColumnType("TEXT"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.Property("UpdatedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EventTypeId"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.DetailType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("IntensitySortType") + .IsRequired() + .HasColumnType("nvarchar(50)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ResourceId") + .HasColumnType("TEXT"); + + b.Property("UpdatedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("DetailTypes"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.EventType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ResourceId") + .HasColumnType("TEXT"); + + b.Property("UpdatedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("EventTypes"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.Intensity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DetailTypeId") + .HasColumnType("INTEGER"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ResourceId") + .HasColumnType("TEXT"); + + b.Property("UpdatedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DetailTypeId"); + + b.ToTable("Intensities"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.Detail", b => + { + b.HasOne("EventJournal.Data.Entities.UserTypes.DetailType", "DetailType") + .WithMany() + .HasForeignKey("DetailTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("EventJournal.Data.Entities.Event", "Event") + .WithMany("Details") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("EventJournal.Data.Entities.UserTypes.Intensity", "Intensity") + .WithMany() + .HasForeignKey("IntensityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DetailType"); + + b.Navigation("Event"); + + b.Navigation("Intensity"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.Event", b => + { + b.HasOne("EventJournal.Data.Entities.UserTypes.EventType", "EventType") + .WithMany() + .HasForeignKey("EventTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EventType"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.Intensity", b => + { + b.HasOne("EventJournal.Data.Entities.UserTypes.DetailType", "DetailType") + .WithMany("AllowedIntensities") + .HasForeignKey("DetailTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DetailType"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.Event", b => + { + b.Navigation("Details"); + }); + + modelBuilder.Entity("EventJournal.Data.Entities.UserTypes.DetailType", b => + { + b.Navigation("AllowedIntensities"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/EventJournal.Data/Migrations/20250831211755_restructureEntities.cs b/EventJournal.Data/Migrations/20250831211755_restructureEntities.cs new file mode 100644 index 0000000..390c5d6 --- /dev/null +++ b/EventJournal.Data/Migrations/20250831211755_restructureEntities.cs @@ -0,0 +1,34 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace EventJournal.Data.Migrations +{ + /// + public partial class restructureEntities : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "IntensitySortType", + table: "DetailTypes", + type: "nvarchar(50)", + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(32)"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "IntensitySortType", + table: "DetailTypes", + type: "nvarchar(32)", + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(50)"); + } + } +} diff --git a/EventJournal.Data/Migrations/DatabaseContextModelSnapshot.cs b/EventJournal.Data/Migrations/DatabaseContextModelSnapshot.cs index 1c69137..6adb916 100644 --- a/EventJournal.Data/Migrations/DatabaseContextModelSnapshot.cs +++ b/EventJournal.Data/Migrations/DatabaseContextModelSnapshot.cs @@ -106,7 +106,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IntensitySortType") .IsRequired() - .HasColumnType("nvarchar(32)"); + .HasColumnType("nvarchar(50)"); b.Property("Name") .IsRequired() diff --git a/EventJournal.DomainModels/DetailDto.cs b/EventJournal.DomainModels/DetailDto.cs index 2f4a7e9..95fe676 100644 --- a/EventJournal.DomainModels/DetailDto.cs +++ b/EventJournal.DomainModels/DetailDto.cs @@ -5,9 +5,6 @@ namespace EventJournal.DomainDto { public class DetailDto : BaseDto { public override Guid ResourceId { get; set; } - [Required] - public required EventDto EventDto { get; set; } - [Required] public virtual required DetailTypeDto DetailType { get; set; } @@ -17,14 +14,6 @@ public class DetailDto : BaseDto { [MaxLength(512)] public string? Notes { get; set; } - public static readonly DetailDto DefaultDetailDto = new() { - ResourceId = Guid.Parse("00000000-0000-0000-0000-000000000001"), - EventDto = EventDto.DefaultEventDto, - DetailType = DetailTypeDto.DefaultDetailTypeDto, - Intensity = DetailTypeDto.DefaultDetailTypeDto.AllowedIntensities.First(), - Notes = "Congratulations on starting your Event History!\nTake the next step and add another event!" - }; - internal override void CopyUserValues(T source) { var sourceDetail = source as DetailDto ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(DetailDto)}"); DetailType = sourceDetail.DetailType; diff --git a/EventJournal.DomainModels/EventDto.cs b/EventJournal.DomainModels/EventDto.cs index 409eacb..d26dcc1 100644 --- a/EventJournal.DomainModels/EventDto.cs +++ b/EventJournal.DomainModels/EventDto.cs @@ -18,14 +18,6 @@ public class EventDto : BaseDto { public IEnumerable Details { get; set; } = []; - public static readonly EventDto DefaultEventDto = new() { - ResourceId = Guid.Parse("00000000-0000-0000-0000-000000000001"), - StartTime = DateTime.Now, - Description = "Event History Started", - EventType = EventTypeDto.DefaultEventTypeDtos.First(), - //Details = [DetailDto.DefaultDetailDto] - }; - internal override void CopyUserValues(T source) { var soruceEvent = source as EventDto ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(EventDto)}"); EventType = soruceEvent.EventType; diff --git a/EventJournal.DomainService/DomainMapperProfile.cs b/EventJournal.DomainService/DomainMapperProfile.cs index 26a6a6c..f770b18 100644 --- a/EventJournal.DomainService/DomainMapperProfile.cs +++ b/EventJournal.DomainService/DomainMapperProfile.cs @@ -9,6 +9,7 @@ public class DomainMapperProfile : Profile { public DomainMapperProfile() { //TODO: use reflection to find objects that inherit from BaseDto and map them + //TODO: also move to bootstrapper CreateMap().ReverseMap(); CreateMap().ReverseMap(); CreateMap().ReverseMap(); diff --git a/EventJournal.DomainService/EventService.cs b/EventJournal.DomainService/EventService.cs index 1f620b1..1d2ffd7 100644 --- a/EventJournal.DomainService/EventService.cs +++ b/EventJournal.DomainService/EventService.cs @@ -1,15 +1,13 @@ using AutoMapper; using EventJournal.Data; using EventJournal.Data.Entities; -using EventJournal.Data.Entities.UserTypes; -using EventJournal.Data.UserTypeRepositories; using EventJournal.DomainDto; using EventJournal.DomainDto.UserTypes; +using EventJournal.DomainService.Exceptions; namespace EventJournal.DomainService { public class EventService( IEventRepository eventRepository, - IEventTypeRepository eventTypeRepository, IMapper mapper) : IEventService { @@ -25,17 +23,32 @@ public async Task> GetAllEventsAsync() { public async Task AddUpdateEventAsync(EventDto dto) { ArgumentNullException.ThrowIfNull(dto); //TODO: additional validations? - var eventEntity = await eventRepository.AddUpdateAsync(mapper.Map(dto)).ConfigureAwait(false); + var savedEntity = await AddUpdateEventInternalAsync(mapper.Map(dto)).ConfigureAwait(false); await eventRepository.SaveChangesAsync().ConfigureAwait(false); - return mapper.Map(eventEntity); - + return mapper.Map(savedEntity); } + private Task AddUpdateEventInternalAsync(Event entity) { + // don't duplicate details - match on either Id or ResourceId + foreach (var detail in entity.Details) { + detail.Event = entity; + var existingDetail = entity.Details.FirstOrDefault(d => d.Id == detail.Id || d.ResourceId == detail.ResourceId); + if (existingDetail != null) { + detail.Id = existingDetail.Id; + detail.ResourceId = existingDetail.ResourceId; + } + } + return eventRepository.AddUpdateAsync(entity); + } public async Task> AddUpdateEventsAsync(IEnumerable dtos) { ArgumentNullException.ThrowIfNull(dtos); - var eventEntities = await eventRepository.AddUpdateAsync(mapper.Map>(dtos)).ConfigureAwait(false); + var events = new List(); + foreach (var dto in dtos) { + Event @event = await AddUpdateEventInternalAsync(mapper.Map(dto)).ConfigureAwait(false); + events.Add(@event); + } await eventRepository.SaveChangesAsync().ConfigureAwait(false); - return mapper.Map>(eventEntities); + return mapper.Map>(events); } public async Task DeleteEventAsync(Guid resourceId) { @@ -43,47 +56,56 @@ public async Task DeleteEventAsync(Guid resourceId) { if (entity == null) { return; } + //TODO: does this leave orphaned details? eventRepository.Delete(entity); await eventRepository.SaveChangesAsync().ConfigureAwait(false); } - // ======================> Event Types <====================== - public async Task> GetAllEventTypesAsync() { - return mapper.Map>(await eventTypeRepository.GetAllAsync().ConfigureAwait(false)); - } - - public async Task GetEventTypeByIdAsync(Guid resourceId) { - return mapper.Map(await eventTypeRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false)); + public async Task AddUpdateDetailAsync(Guid eventResourceId, DetailDto detailDto) { + ArgumentNullException.ThrowIfNull(detailDto); + var eventEntity = await eventRepository.GetByResourceIdAsync(eventResourceId).ConfigureAwait(false) ?? throw new ResourceNotFoundException($"Event with ResourceId {eventResourceId} not found."); + var result = eventEntity.AddUpdateDetail(mapper.Map(detailDto)); + await eventRepository.SaveChangesAsync().ConfigureAwait(false); + return result; } - public async Task AddUpdateEventTypeAsync(EventTypeDto dto) { - ArgumentNullException.ThrowIfNull(dto); - //TODO: additional validations? - var eventTypeEntity = await eventTypeRepository.AddUpdateAsync(mapper.Map(dto)).ConfigureAwait(false); - await eventTypeRepository.SaveChangesAsync().ConfigureAwait(false); - return mapper.Map(eventTypeEntity); + public async Task AddUpdateDetailsAsync(Guid eventResourceId, IEnumerable detailDtos) { + ArgumentNullException.ThrowIfNull(detailDtos); + var eventEntity = await eventRepository.GetByResourceIdAsync(eventResourceId).ConfigureAwait(false) ?? throw new ResourceNotFoundException($"Event with ResourceId {eventResourceId} not found."); + eventEntity.AddUpdateDetails(mapper.Map>(detailDtos)); + await eventRepository.SaveChangesAsync().ConfigureAwait(false); } - public async Task> AddUpdateEventTypesAsync(IEnumerable dtos) { - ArgumentNullException.ThrowIfNull(dtos); - var eventTypeEntities = await eventTypeRepository.AddUpdateAsync(mapper.Map>(dtos)).ConfigureAwait(false); - await eventTypeRepository.SaveChangesAsync().ConfigureAwait(false); - return mapper.Map>(eventTypeEntities); + public async Task RemoveDetailAsync(Guid eventResourceId, Guid detailResourceId) { + var eventEntity = await eventRepository.GetByResourceIdAsync(eventResourceId).ConfigureAwait(false) ?? throw new ResourceNotFoundException($"Event with ResourceId {eventResourceId} not found."); + eventEntity.RemoveDetail(detailResourceId); + await eventRepository.SaveChangesAsync().ConfigureAwait(false); } - public async Task DeleteEventTypeAsync(Guid resourceId) { - var entity = await eventTypeRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false); - if (entity == null) { - return; - } - eventTypeRepository.Delete(entity); - await eventTypeRepository.SaveChangesAsync().ConfigureAwait(false); + public async Task RemoveAllDetailsAsync(Guid eventResourceId) { + var eventEntity = await eventRepository.GetByResourceIdAsync(eventResourceId).ConfigureAwait(false) ?? throw new ResourceNotFoundException($"Event with ResourceId {eventResourceId} not found."); + eventEntity.RemoveAllDetails(); + await eventRepository.SaveChangesAsync().ConfigureAwait(false); } - public async Task AddTestDataAsync() { - await AddUpdateEventTypesAsync(EventTypeDto.DefaultEventTypeDtos).ConfigureAwait(false); - await AddUpdateEventAsync(EventDto.DefaultEventDto); + public static readonly Guid DefaultEventResourceId = Guid.Parse("00000000-0000-0000-0000-000000000001"); + public static readonly Guid DefaultDetailResourceId = Guid.Parse("00000000-0000-0000-0000-000000000001"); + public Task AddResetTestDataAsync() { + + return AddUpdateEventAsync(new EventDto { + ResourceId = DefaultEventResourceId, + StartTime = DateTime.Now, + Description = "Event History Started", + EventType = EventTypeDto.DefaultEventTypeDtos.First(), + Details = [ new DetailDto { + ResourceId = DefaultDetailResourceId, + DetailType = DetailTypeDto.DefaultDetailTypeDto, + Intensity = DetailTypeDto.DefaultDetailTypeDto.AllowedIntensities.First(), + Notes = "Congratulations on starting your Event History!\nTake the next step and add another event!" + } + ] + }); } } } diff --git a/EventJournal.DomainService/IEventService.cs b/EventJournal.DomainService/IEventService.cs index a471e95..fe49998 100644 --- a/EventJournal.DomainService/IEventService.cs +++ b/EventJournal.DomainService/IEventService.cs @@ -1,5 +1,5 @@ -using EventJournal.DomainDto; -using EventJournal.DomainDto.UserTypes; +using EventJournal.Data.Entities; +using EventJournal.DomainDto; namespace EventJournal.DomainService { public interface IEventService { @@ -10,13 +10,10 @@ public interface IEventService { Task> AddUpdateEventsAsync(IEnumerable dtos); Task DeleteEventAsync(Guid resourceId); - // ======================> Event Types <====================== - Task> GetAllEventTypesAsync(); - Task GetEventTypeByIdAsync(Guid resourceId); - Task AddUpdateEventTypeAsync(EventTypeDto dto); - Task> AddUpdateEventTypesAsync(IEnumerable dtos); - Task DeleteEventTypeAsync(Guid resourceId); - - Task AddTestDataAsync(); + Task AddResetTestDataAsync(); + Task AddUpdateDetailAsync(Guid eventResourceId, DetailDto detailDto); + Task AddUpdateDetailsAsync(Guid eventResourceId, IEnumerable detailDtos); + Task RemoveDetailAsync(Guid eventResourceId, Guid detailResourceId); + Task RemoveAllDetailsAsync(Guid eventResourceId); } } \ No newline at end of file diff --git a/EventJournal.DomainService/IDetailService.cs b/EventJournal.DomainService/IUserTypesService.cs similarity index 68% rename from EventJournal.DomainService/IDetailService.cs rename to EventJournal.DomainService/IUserTypesService.cs index 03848aa..8dfacb8 100644 --- a/EventJournal.DomainService/IDetailService.cs +++ b/EventJournal.DomainService/IUserTypesService.cs @@ -2,14 +2,15 @@ using EventJournal.DomainDto.UserTypes; namespace EventJournal.DomainService { - public interface IDetailService { + public interface IUserTypesService { - // ======================> Details <====================== - Task> GetAllDetailsAsync(); - Task GetDetailByIdAsync(Guid resourceId); - Task AddUpdateDetailAsync(DetailDto dto); - Task> AddUpdateDetailsAsync(IEnumerable dtos); - Task DeleteDetailAsync(Guid resourceId); + + // ======================> Event Types <====================== + Task> GetAllEventTypesAsync(); + Task GetEventTypeByIdAsync(Guid resourceId); + Task AddUpdateEventTypeAsync(EventTypeDto dto); + Task> AddUpdateEventTypesAsync(IEnumerable dtos); + Task DeleteEventTypeAsync(Guid resourceId); // ======================> Detail Types <====================== Task> GetAllDetailTypesAsync(); diff --git a/EventJournal.DomainService/DetailService.cs b/EventJournal.DomainService/UserTypesService.cs similarity index 72% rename from EventJournal.DomainService/DetailService.cs rename to EventJournal.DomainService/UserTypesService.cs index c206401..c5f8ef8 100644 --- a/EventJournal.DomainService/DetailService.cs +++ b/EventJournal.DomainService/UserTypesService.cs @@ -7,45 +7,45 @@ using EventJournal.DomainDto.UserTypes; namespace EventJournal.DomainService { - public class DetailService( - IDetailRepository detailRepository, + public class UserTypesService( + IEventTypeRepository eventTypeRepository, IDetailTypeRepository detailTypeRepository, IIntensityRepository intensityRepository, IMapper mapper) - : IDetailService { + : IUserTypesService { - // ======================> Details <====================== - public async Task> GetAllDetailsAsync() { - return mapper.Map>(await detailRepository.GetAllAsync().ConfigureAwait(false)); + // ======================> Event Types <====================== + public async Task> GetAllEventTypesAsync() { + return mapper.Map>(await eventTypeRepository.GetAllAsync().ConfigureAwait(false)); } - public async Task GetDetailByIdAsync(Guid resourceId) { - return mapper.Map(await detailRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false)); + public async Task GetEventTypeByIdAsync(Guid resourceId) { + return mapper.Map(await eventTypeRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false)); } - public async Task AddUpdateDetailAsync(DetailDto dto) { + public async Task AddUpdateEventTypeAsync(EventTypeDto dto) { ArgumentNullException.ThrowIfNull(dto); //TODO: additional validations? - var entity = await detailRepository.AddUpdateAsync(mapper.Map(dto)).ConfigureAwait(false); - await detailRepository.SaveChangesAsync().ConfigureAwait(false); - return mapper.Map(entity); + var eventTypeEntity = await eventTypeRepository.AddUpdateAsync(mapper.Map(dto)).ConfigureAwait(false); + await eventTypeRepository.SaveChangesAsync().ConfigureAwait(false); + return mapper.Map(eventTypeEntity); } - public async Task> AddUpdateDetailsAsync(IEnumerable dtos) { + public async Task> AddUpdateEventTypesAsync(IEnumerable dtos) { ArgumentNullException.ThrowIfNull(dtos); - var detailEntities = await detailRepository.AddUpdateAsync(mapper.Map>(dtos)).ConfigureAwait(false); - await detailRepository.SaveChangesAsync().ConfigureAwait(false); - return mapper.Map>(detailEntities); + var eventTypeEntities = await eventTypeRepository.AddUpdateAsync(mapper.Map>(dtos)).ConfigureAwait(false); + await eventTypeRepository.SaveChangesAsync().ConfigureAwait(false); + return mapper.Map>(eventTypeEntities); } - public async Task DeleteDetailAsync(Guid resourceId) { - var entity = await detailRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false); + public async Task DeleteEventTypeAsync(Guid resourceId) { + var entity = await eventTypeRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false); if (entity == null) { return; } - detailRepository.Delete(entity); - await detailRepository.SaveChangesAsync().ConfigureAwait(false); + eventTypeRepository.Delete(entity); + await eventTypeRepository.SaveChangesAsync().ConfigureAwait(false); } // ======================> Detail Types <====================== @@ -115,9 +115,9 @@ public async Task DeleteIntensityAsync(Guid resourceId) { } public async Task AddTestDataAsync() { + await AddUpdateEventTypesAsync(EventTypeDto.DefaultEventTypeDtos).ConfigureAwait(false); await AddUpdateDetailTypeAsync(DetailTypeDto.DefaultDetailTypeDto).ConfigureAwait(false); await AddUpdateIntensitiesAsync(IntensityDto.DefaultIntensityDtos).ConfigureAwait(false); - //await AddUpdateDetailAsync(DetailDto.DefaultDetailDto).ConfigureAwait(false); } } } diff --git a/README.md b/README.md index 6343751..a499375 100644 --- a/README.md +++ b/README.md @@ -63,11 +63,11 @@ EF https://learn.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs ### NEXT STEPS - Fix repository structure - - move details back to event service/repo and rename detail service to types service and move event types there - - we need a separate types service because types can be added/removed independently of events - - remove detail repository and intensity repository + - ~~move details back to event service/repo and rename detail service to types service and move event types there~~ + - ~~we need a separate types service because types can be added/removed independently of events~~ + - ~~remove detail repository~~ and intensity repository - only add/delete or even GET intensities via methods on detail type - - likewise only add/delete or even GET details via methods on event + - ~~likewise only add/delete or even GET details via methods on event~~ - fix default data (move from DTOs to repository so that individual entities are not duplicated when added via parents) - eg you don't get duplicate event type entity when event is saved - in other words the event type added when event is added isn't a duplicate - test full get event with details From 59b40d4e973dc197db5f97f19b7b4f4fc0ca39ab Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Sun, 31 Aug 2025 17:21:46 -0400 Subject: [PATCH 26/34] WIP update readme to relrect current state --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index a499375..d36bf97 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,8 @@ EF https://learn.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs - ~~likewise only add/delete or even GET details via methods on event~~ - fix default data (move from DTOs to repository so that individual entities are not duplicated when added via parents) - eg you don't get duplicate event type entity when event is saved - in other words the event type added when event is added isn't a duplicate + - ~~move default event and default details into event service~~ + - move all default user type data into user types service (follow event service's example for default data) - test full get event with details - test full get detail type with allowed intensities - add multiple default detail types to initializer (a la event types) From bf25aabec667da992d91c8a02d815cf492a99ebd Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Mon, 1 Sep 2025 00:53:01 -0400 Subject: [PATCH 27/34] WIP repo/entity structrue fixed - well almost --- EventJournal.CLI/Program.cs | 18 ++-- EventJournal.Data/Entities/Event.cs | 15 +-- .../Entities/UserTypes/DetailType.cs | 33 ++++++- .../IIntensityRepository.cs | 6 -- .../IntensityRepository.cs | 18 ---- EventJournal.DomainModels/EventDto.cs | 2 + .../UserTypes/DetailTypeDto.cs | 9 +- .../UserTypes/EventTypeDto.cs | 15 +-- .../UserTypes/IntensityDto.cs | 50 ---------- .../DefaultDataProvider.cs | 94 +++++++++++++++++++ EventJournal.DomainService/EventService.cs | 38 +++----- .../IDefaultDataProvider.cs | 6 ++ EventJournal.DomainService/IEventService.cs | 5 +- .../IUserTypesService.cs | 16 +--- .../UserTypesService.cs | 90 +++++++++--------- README.md | 9 +- 16 files changed, 218 insertions(+), 206 deletions(-) delete mode 100644 EventJournal.Data/UserTypeRepositories/IIntensityRepository.cs delete mode 100644 EventJournal.Data/UserTypeRepositories/IntensityRepository.cs create mode 100644 EventJournal.DomainService/DefaultDataProvider.cs create mode 100644 EventJournal.DomainService/IDefaultDataProvider.cs diff --git a/EventJournal.CLI/Program.cs b/EventJournal.CLI/Program.cs index 896e3b8..706c9a5 100644 --- a/EventJournal.CLI/Program.cs +++ b/EventJournal.CLI/Program.cs @@ -13,8 +13,9 @@ internal class Program { private static async Task Main(string[] args) { var services = CreateServiceCollection(); - var eventService = services.GetService() ?? throw new Exception("Unable to locate a valid Product Logic module"); - var userTypeService = services.GetService() ?? throw new Exception("Unable to locate a valid Order Logic module"); + var eventService = services.GetService() ?? throw new Exception("Unable to locate a valid Event Service"); + var userTypeService = services.GetService() ?? throw new Exception("Unable to locate a valid User Types Service"); + var defaultDataProvider = services.GetService() ?? throw new Exception("Unable to locate a valid Default Data Provider"); bool userIsDone = false; while (!userIsDone) { //Console.WriteLine("Type '1' to "); @@ -66,7 +67,7 @@ private static async Task Main(string[] args) { await ViewallDataAsync(GetSerializerOptions(), eventService, userTypeService).ConfigureAwait(false); break; case 'a': - await AddTestDataAsync(eventService, userTypeService).ConfigureAwait(false); + await AddTestDataAsync(defaultDataProvider).ConfigureAwait(false); break; case 'x': await DeleteAllDataAsync(eventService, userTypeService).ConfigureAwait(false); @@ -85,9 +86,9 @@ static IServiceProvider CreateServiceCollection() { .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddLogging(options => { options.AddDebug(); options.SetMinimumLevel(LogLevel.Error); @@ -100,10 +101,9 @@ static IServiceProvider CreateServiceCollection() { return servicecollection.BuildServiceProvider(); } - static async Task AddTestDataAsync(IEventService eventService, IUserTypesService userTypeService) { + static Task AddTestDataAsync(IDefaultDataProvider defaultDataProvider) { Console.WriteLine("Adding/Resetting test data."); - await userTypeService.AddTestDataAsync().ConfigureAwait(false); - await eventService.AddResetTestDataAsync().ConfigureAwait(false); + return defaultDataProvider.AddResetDefaultDataAsync(); } static async Task DeleteAllDataAsync(IEventService eventService, IUserTypesService userTypeService) { @@ -114,10 +114,6 @@ static async Task DeleteAllDataAsync(IEventService eventService, IUserTypesServi foreach (var e in await userTypeService.GetAllEventTypesAsync().ConfigureAwait(false)) { await userTypeService.DeleteEventTypeAsync(e.ResourceId).ConfigureAwait(false); } - - foreach (var e in await userTypeService.GetAllIntensitiesAsync().ConfigureAwait(false)) { - await userTypeService.DeleteIntensityAsync(e.ResourceId).ConfigureAwait(false); - } foreach (var e in await userTypeService.GetAllDetailTypesAsync().ConfigureAwait(false)) { await userTypeService.DeleteDetailTypeAsync(e.ResourceId).ConfigureAwait(false); } diff --git a/EventJournal.Data/Entities/Event.cs b/EventJournal.Data/Entities/Event.cs index ff264d9..f344338 100644 --- a/EventJournal.Data/Entities/Event.cs +++ b/EventJournal.Data/Entities/Event.cs @@ -19,19 +19,20 @@ public class Event : BaseEntity { [MaxLength(500)] public string? Description { get; set; } - public IReadOnlyCollection Details=> (IReadOnlyCollection)_details; + public IReadOnlyCollection Details => (IReadOnlyCollection)_details; private IList _details = []; public Detail AddUpdateDetail(Detail detail) { ArgumentNullException.ThrowIfNull(detail, nameof(detail)); var existingDetail = _details.FirstOrDefault(d => d.Id == detail.Id || d.ResourceId == detail.ResourceId); - if (existingDetail == null) { - detail.Event = this; - _details.Add(detail); - return detail; + if (existingDetail != null) { + //TODO: shouldn't this already be the case? + existingDetail.Event = this; + return existingDetail.UpdateEntity(detail); } - existingDetail.Event = this; - return existingDetail.UpdateEntity(detail); + detail.Event = this; + _details.Add(detail); + return detail; } public void AddUpdateDetails(IEnumerable details) { diff --git a/EventJournal.Data/Entities/UserTypes/DetailType.cs b/EventJournal.Data/Entities/UserTypes/DetailType.cs index cd92be5..b7fd136 100644 --- a/EventJournal.Data/Entities/UserTypes/DetailType.cs +++ b/EventJournal.Data/Entities/UserTypes/DetailType.cs @@ -20,7 +20,38 @@ public class DetailType : BaseEntity { [Column(TypeName = "nvarchar(50)")] public SortType IntensitySortType { get; set; } = SortType.None; - public ICollection AllowedIntensities { get; set; } = []; + public IReadOnlyCollection AllowedIntensities => (IReadOnlyCollection)_intensities; + private IList _intensities = []; + + public Intensity AddUpdateAllowedIntensity(Intensity intensity) { + ArgumentNullException.ThrowIfNull(intensity, nameof(intensity)); + var existingIntensity = _intensities.FirstOrDefault(i => i.Id == intensity.Id || i.ResourceId == intensity.ResourceId); + if (existingIntensity != null) { + //TODO: shouldn't this already be the case? + existingIntensity.DetailType = this; + return existingIntensity.UpdateEntity(intensity); + } + intensity.DetailType = this; + _intensities.Add(intensity); + return intensity; + } + + public void AddUpdateAllowedIntensities(IEnumerable intensities) { + ArgumentNullException.ThrowIfNull(intensities, nameof(intensities)); + foreach (var intensity in intensities) { + AddUpdateAllowedIntensity(intensity); + } + } + + public void RemoveIntensity(Guid intensityResoruceId) { + var existignIntensity = _intensities.FirstOrDefault(i => i.ResourceId == intensityResoruceId); + if (existignIntensity != null) { + _intensities.Remove(existignIntensity); + } + } + public void RemoveAllIntensities() { + _intensities.Clear(); + } internal override void CopyUserValues(T source) { var sourceDetailType = source as DetailType ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(DetailType)}"); diff --git a/EventJournal.Data/UserTypeRepositories/IIntensityRepository.cs b/EventJournal.Data/UserTypeRepositories/IIntensityRepository.cs deleted file mode 100644 index 2a37373..0000000 --- a/EventJournal.Data/UserTypeRepositories/IIntensityRepository.cs +++ /dev/null @@ -1,6 +0,0 @@ -using EventJournal.Data.Entities.UserTypes; - -namespace EventJournal.Data.UserTypeRepositories { - public interface IIntensityRepository : IBaseRepository { - } -} \ No newline at end of file diff --git a/EventJournal.Data/UserTypeRepositories/IntensityRepository.cs b/EventJournal.Data/UserTypeRepositories/IntensityRepository.cs deleted file mode 100644 index 45aa43f..0000000 --- a/EventJournal.Data/UserTypeRepositories/IntensityRepository.cs +++ /dev/null @@ -1,18 +0,0 @@ -using EventJournal.Data.Entities.UserTypes; -using Microsoft.EntityFrameworkCore; - -namespace EventJournal.Data.UserTypeRepositories { - public class IntensityRepository(IDatabaseContext db) : BaseRepository(db, db.Intensities), IIntensityRepository { - public override async Task> GetAllAsync() { - return await table - .OrderBy(i => i.Level) - .ToListAsync() - .ConfigureAwait(false); - } - - public override Task GetByResourceIdAsync(Guid resourceId) { - return table - .FirstOrDefaultAsync(i => i.ResourceId == resourceId); - } - } -} diff --git a/EventJournal.DomainModels/EventDto.cs b/EventJournal.DomainModels/EventDto.cs index d26dcc1..109be72 100644 --- a/EventJournal.DomainModels/EventDto.cs +++ b/EventJournal.DomainModels/EventDto.cs @@ -24,6 +24,8 @@ internal override void CopyUserValues(T source) { StartTime = soruceEvent.StartTime; EndTime = soruceEvent.EndTime; Description = soruceEvent.Description; + //TODO: this might need to be a deep copy depending on usage + // or might need to copy by value instead of reference (eg. call inteisity.copyvaules for each item) Details = soruceEvent.Details; } diff --git a/EventJournal.DomainModels/UserTypes/DetailTypeDto.cs b/EventJournal.DomainModels/UserTypes/DetailTypeDto.cs index e174f8c..4b7d848 100644 --- a/EventJournal.DomainModels/UserTypes/DetailTypeDto.cs +++ b/EventJournal.DomainModels/UserTypes/DetailTypeDto.cs @@ -17,14 +17,6 @@ public class DetailTypeDto : BaseDto { [Required] public required SortType IntensitySortType { get; set; } = SortType.Descending; - public static readonly DetailTypeDto DefaultDetailTypeDto = new() { - ResourceId = Guid.Parse("00000000-0000-0000-0000-000000000001"), - Description = "This is a generic detail type", - Name = "Generic Detail Type", - IntensitySortType = SortType.Descending, - AllowedIntensities = IntensityDto.DefaultIntensityDtos - }; - internal override void CopyUserValues(T source) { var sourceDetailType = source as DetailTypeDto ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(DetailTypeDto)}"); Name = sourceDetailType.Name; @@ -34,6 +26,7 @@ internal override void CopyUserValues(T source) { // or might need to copy by value instead of reference (eg. call inteisity.copyvaules for each item) AllowedIntensities = sourceDetailType.AllowedIntensities; } + public override string ToString() { return this.Serialize(); } diff --git a/EventJournal.DomainModels/UserTypes/EventTypeDto.cs b/EventJournal.DomainModels/UserTypes/EventTypeDto.cs index 9e71a6a..22f1c34 100644 --- a/EventJournal.DomainModels/UserTypes/EventTypeDto.cs +++ b/EventJournal.DomainModels/UserTypes/EventTypeDto.cs @@ -10,25 +10,12 @@ public class EventTypeDto : BaseDto { [MaxLength(500)] public string? Description { get; set; } - public static readonly EventTypeDto DefaultEventTypeDto = new() { - ResourceId = Guid.Parse("00000000-0000-0000-0000-000000000001"), - Name = "Random Event", - Description = "Use for tracking random things like onset of pain, headache, or whatever that isn't directly associated with a specific event type." - }; - - public static readonly EventTypeDto[] DefaultEventTypeDtos = [ - DefaultEventTypeDto, - new EventTypeDto { ResourceId = Guid.Parse("00000000-0000-0000-0000-000000000002"), Name = "Exercise" }, - new EventTypeDto { ResourceId = Guid.Parse("00000000-0000-0000-0000-000000000003"), Name = "Bathroom Visit" }, - new EventTypeDto { ResourceId = Guid.Parse("00000000-0000-0000-0000-000000000004"), Name = "Food Consumption" }, - new EventTypeDto { ResourceId = Guid.Parse("00000000-0000-0000-0000-000000000005"), Name = "Weigh In" }, - ]; - internal override void CopyUserValues(T source) { var sourceEventType = source as EventTypeDto ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(EventTypeDto)}"); Name = sourceEventType.Name; Description = sourceEventType.Description; } + public override string ToString() { return this.Serialize(); } diff --git a/EventJournal.DomainModels/UserTypes/IntensityDto.cs b/EventJournal.DomainModels/UserTypes/IntensityDto.cs index 991f1f0..09e35f4 100644 --- a/EventJournal.DomainModels/UserTypes/IntensityDto.cs +++ b/EventJournal.DomainModels/UserTypes/IntensityDto.cs @@ -13,56 +13,6 @@ public class IntensityDto : BaseDto { [MaxLength(500)] public string? Description { get; set; } - public required DetailTypeDto DetailTypeDto { get; set; } - - public static readonly IntensityDto ZeroIntensityDto = new() { - ResourceId = Guid.Parse("00000000-0000-0000-0000-000000000001"), - Level = 0, - Name = "Zero Intensity", - Description = "Not intense at all. Normal.", - DetailTypeDto = DetailTypeDto.DefaultDetailTypeDto - }; - - public static readonly IntensityDto MildIntensityDto = new() { - ResourceId = Guid.Parse("00000000-0000-0000-0000-000000000002"), - Level = 1, - Name = "Mild Intensity", - Description = "Slightly intense, but nothing special.", - DetailTypeDto = DetailTypeDto.DefaultDetailTypeDto - }; - - public static readonly IntensityDto ModerateIntensityDto = new() { - ResourceId = Guid.Parse("00000000-0000-0000-0000-000000000003"), - Level = 2, - Name = "Moderate Intensity", - Description = "Intense but not very unbearable or uncomfortable.", - DetailTypeDto = DetailTypeDto.DefaultDetailTypeDto - }; - - public static readonly IntensityDto HighIntensityDto = new() { - ResourceId = Guid.Parse("00000000-0000-0000-0000-000000000004"), - Level = 3, - Name = "High Intensity", - Description = "Intensity that is uncomfortable but still bearable.", - DetailTypeDto = DetailTypeDto.DefaultDetailTypeDto - }; - - public static readonly IntensityDto InsaneIntensityDto = new() { - ResourceId = Guid.Parse("00000000-0000-0000-0000-000000000005"), - Level = 4, - Name = "Insane Intensity", - Description = "Unbearable. Insane. No one needs to experience this.", - DetailTypeDto = DetailTypeDto.DefaultDetailTypeDto - }; - - public static readonly IntensityDto[] DefaultIntensityDtos = [ - ZeroIntensityDto, - MildIntensityDto, - ModerateIntensityDto, - HighIntensityDto, - InsaneIntensityDto - ]; - internal override void CopyUserValues(T source) { var sourceIntensity = source as IntensityDto ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(IntensityDto)}"); Name = sourceIntensity.Name; diff --git a/EventJournal.DomainService/DefaultDataProvider.cs b/EventJournal.DomainService/DefaultDataProvider.cs new file mode 100644 index 0000000..ed81d7d --- /dev/null +++ b/EventJournal.DomainService/DefaultDataProvider.cs @@ -0,0 +1,94 @@ +using EventJournal.Common.Enumerations; +using EventJournal.DomainDto; +using EventJournal.DomainDto.UserTypes; + +namespace EventJournal.DomainService { + public class DefaultDataProvider( + IEventService eventService, + IUserTypesService userTypesService) + : IDefaultDataProvider { + private static readonly Guid DefaultEventResourceId = Guid.Parse("00000000-0000-0000-0000-000000000001"); + private static readonly Guid DefaultDetailResourceId = Guid.Parse("00000000-0000-0000-0000-000000000001"); + private static readonly Guid[] DefaultEventTypeResourceIds = [ + Guid.Parse("00000000-0000-0000-0000-000000000001"), + Guid.Parse("00000000-0000-0000-0000-000000000002"), + Guid.Parse("00000000-0000-0000-0000-000000000003"), + Guid.Parse("00000000-0000-0000-0000-000000000004"), + Guid.Parse("00000000-0000-0000-0000-000000000005") + ]; + private static readonly Guid DefaultDetailTypeResourceId = Guid.Parse("00000000-0000-0000-0000-000000000001"); + + private static readonly Guid[] DefaultIntensityResourceIds = [ + Guid.Parse("00000000-0000-0000-0000-000000000001"), + Guid.Parse("00000000-0000-0000-0000-000000000002"), + Guid.Parse("00000000-0000-0000-0000-000000000003"), + Guid.Parse("00000000-0000-0000-0000-000000000004"), + Guid.Parse("00000000-0000-0000-0000-000000000005"), + ]; + + public async Task AddResetDefaultDataAsync() { + var defaultEventTypes = await userTypesService.AddUpdateEventTypesAsync([ + new EventTypeDto{ ResourceId = DefaultEventTypeResourceIds[0], Name = "Random Event", Description="Use for tracking random things like onset of pain, headache, or whatever that isn't directly associated with a specific event type." }, + new EventTypeDto{ ResourceId = DefaultEventTypeResourceIds[1], Name = "Bowel Movement", Description=""}, + new EventTypeDto{ ResourceId = DefaultEventTypeResourceIds[2], Name = "Ate Something", Description=""}, + new EventTypeDto{ ResourceId = DefaultEventTypeResourceIds[3], Name = "Weigh In", Description=""}, + new EventTypeDto{ ResourceId = DefaultEventTypeResourceIds[4], Name = "Exercise", Description=""} + ]).ConfigureAwait(false); + + var defaultDetailTypeDto = await userTypesService.AddUpdateDetailTypeAsync(new DetailTypeDto { + ResourceId = DefaultDetailTypeResourceId, + Description = "This is a generic detail type", + Name = "Generic Detail Type", + IntensitySortType = SortType.Descending, + AllowedIntensities = [ + new() { + ResourceId = DefaultIntensityResourceIds[0], + Level = 0, + Name = "Zero Intensity", + Description = "Not intense at all. Normal." + }, + new() { + ResourceId = DefaultIntensityResourceIds[1], + Level = 1, + Name = "Mild Intensity", + Description = "Almost broke a sweat." + }, + new() { + ResourceId = DefaultIntensityResourceIds[2], + Level = 2, + Name = "Moderate Intensity", + Description = "Got sweaty, did some breathing. Might feel this tomorrow." + }, + new() { + ResourceId = DefaultIntensityResourceIds[3], + Level = 3, + Name = "High Intensity", + Description = "Breathing hard. Will definitely feel this for a few days." + }, + new() { + ResourceId = DefaultIntensityResourceIds[4], + Level = 4, + Name = "Insane Intensity", + Description = "Unbearable. Insane. No one needs to experience this. Why did I do this to myself?" + } + ] + }).ConfigureAwait(false); + + + var eventDto = await eventService.AddUpdateEventAsync(new EventDto { + ResourceId = DefaultEventResourceId, + StartTime = DateTime.Now, + Description = "Event History Started", + EventType = defaultEventTypes.First() + }); + + await eventService.AddUpdateDetailAsync(eventDto.ResourceId, new DetailDto { + ResourceId = DefaultDetailResourceId, + DetailType = defaultDetailTypeDto, + Intensity = defaultDetailTypeDto.AllowedIntensities.First(), + Notes = "Congratulations on starting your Event History!\nTake the next step and add another event!" + }); + } + + } +} diff --git a/EventJournal.DomainService/EventService.cs b/EventJournal.DomainService/EventService.cs index 1d2ffd7..afd0f38 100644 --- a/EventJournal.DomainService/EventService.cs +++ b/EventJournal.DomainService/EventService.cs @@ -2,12 +2,13 @@ using EventJournal.Data; using EventJournal.Data.Entities; using EventJournal.DomainDto; -using EventJournal.DomainDto.UserTypes; using EventJournal.DomainService.Exceptions; namespace EventJournal.DomainService { public class EventService( IEventRepository eventRepository, + //TODO: fix this services shouldn't call services + IUserTypesService userTypesService, IMapper mapper) : IEventService { @@ -23,11 +24,16 @@ public async Task> GetAllEventsAsync() { public async Task AddUpdateEventAsync(EventDto dto) { ArgumentNullException.ThrowIfNull(dto); //TODO: additional validations? - var savedEntity = await AddUpdateEventInternalAsync(mapper.Map(dto)).ConfigureAwait(false); + var savedEntity = await AddUpdateEventPrivateAsync(mapper.Map(dto)).ConfigureAwait(false); + // so the problem is that we need to find existing DetailType with it's existing AllowedIntensities and THEN assign detail to detail + // and pick the right intensity. Not sure this should happen here maybe in AddUpdateDetailAsync() ?? + // - in other wrds extend the logic in AddUpdteEventPrivateAsync to utilize similar logic that exists in the USErTypesServie for DetailType and Intensities. + // /// remember the goal is that Detail selects a valid intensity from teh list of allowed intensities specified by detail type. + // another stray thought: maybe we need a mapping table that maps Detail id and Intensity id? await eventRepository.SaveChangesAsync().ConfigureAwait(false); return mapper.Map(savedEntity); } - private Task AddUpdateEventInternalAsync(Event entity) { + private Task AddUpdateEventPrivateAsync(Event entity) { // don't duplicate details - match on either Id or ResourceId foreach (var detail in entity.Details) { detail.Event = entity; @@ -44,7 +50,7 @@ public async Task> AddUpdateEventsAsync(IEnumerable(); foreach (var dto in dtos) { - Event @event = await AddUpdateEventInternalAsync(mapper.Map(dto)).ConfigureAwait(false); + Event @event = await AddUpdateEventPrivateAsync(mapper.Map(dto)).ConfigureAwait(false); events.Add(@event); } await eventRepository.SaveChangesAsync().ConfigureAwait(false); @@ -61,12 +67,12 @@ public async Task DeleteEventAsync(Guid resourceId) { await eventRepository.SaveChangesAsync().ConfigureAwait(false); } - public async Task AddUpdateDetailAsync(Guid eventResourceId, DetailDto detailDto) { + public async Task AddUpdateDetailAsync(Guid eventResourceId, DetailDto detailDto) { ArgumentNullException.ThrowIfNull(detailDto); var eventEntity = await eventRepository.GetByResourceIdAsync(eventResourceId).ConfigureAwait(false) ?? throw new ResourceNotFoundException($"Event with ResourceId {eventResourceId} not found."); var result = eventEntity.AddUpdateDetail(mapper.Map(detailDto)); await eventRepository.SaveChangesAsync().ConfigureAwait(false); - return result; + return mapper.Map(result); } public async Task AddUpdateDetailsAsync(Guid eventResourceId, IEnumerable detailDtos) { @@ -87,25 +93,5 @@ public async Task RemoveAllDetailsAsync(Guid eventResourceId) { eventEntity.RemoveAllDetails(); await eventRepository.SaveChangesAsync().ConfigureAwait(false); } - - - public static readonly Guid DefaultEventResourceId = Guid.Parse("00000000-0000-0000-0000-000000000001"); - public static readonly Guid DefaultDetailResourceId = Guid.Parse("00000000-0000-0000-0000-000000000001"); - public Task AddResetTestDataAsync() { - - return AddUpdateEventAsync(new EventDto { - ResourceId = DefaultEventResourceId, - StartTime = DateTime.Now, - Description = "Event History Started", - EventType = EventTypeDto.DefaultEventTypeDtos.First(), - Details = [ new DetailDto { - ResourceId = DefaultDetailResourceId, - DetailType = DetailTypeDto.DefaultDetailTypeDto, - Intensity = DetailTypeDto.DefaultDetailTypeDto.AllowedIntensities.First(), - Notes = "Congratulations on starting your Event History!\nTake the next step and add another event!" - } - ] - }); - } } } diff --git a/EventJournal.DomainService/IDefaultDataProvider.cs b/EventJournal.DomainService/IDefaultDataProvider.cs new file mode 100644 index 0000000..a947646 --- /dev/null +++ b/EventJournal.DomainService/IDefaultDataProvider.cs @@ -0,0 +1,6 @@ + +namespace EventJournal.DomainService { + public interface IDefaultDataProvider { + public Task AddResetDefaultDataAsync(); + } +} \ No newline at end of file diff --git a/EventJournal.DomainService/IEventService.cs b/EventJournal.DomainService/IEventService.cs index fe49998..0cb672c 100644 --- a/EventJournal.DomainService/IEventService.cs +++ b/EventJournal.DomainService/IEventService.cs @@ -9,9 +9,8 @@ public interface IEventService { Task AddUpdateEventAsync(EventDto dto); Task> AddUpdateEventsAsync(IEnumerable dtos); Task DeleteEventAsync(Guid resourceId); - - Task AddResetTestDataAsync(); - Task AddUpdateDetailAsync(Guid eventResourceId, DetailDto detailDto); + + Task AddUpdateDetailAsync(Guid eventResourceId, DetailDto detailDto); Task AddUpdateDetailsAsync(Guid eventResourceId, IEnumerable detailDtos); Task RemoveDetailAsync(Guid eventResourceId, Guid detailResourceId); Task RemoveAllDetailsAsync(Guid eventResourceId); diff --git a/EventJournal.DomainService/IUserTypesService.cs b/EventJournal.DomainService/IUserTypesService.cs index 8dfacb8..baddf92 100644 --- a/EventJournal.DomainService/IUserTypesService.cs +++ b/EventJournal.DomainService/IUserTypesService.cs @@ -1,5 +1,4 @@ -using EventJournal.DomainDto; -using EventJournal.DomainDto.UserTypes; +using EventJournal.DomainDto.UserTypes; namespace EventJournal.DomainService { public interface IUserTypesService { @@ -18,14 +17,9 @@ public interface IUserTypesService { Task AddUpdateDetailTypeAsync(DetailTypeDto dto); Task> AddUpdateDetailTypesAsync(IEnumerable dtos); Task DeleteDetailTypeAsync(Guid resourceId); - - // =======================> Intensities <====================== - Task> GetAllIntensitiesAsync(); - Task GetIntensityByIdAsync(Guid resourceId); - Task AddUpdateIntensityAsync(IntensityDto dto); - Task> AddUpdateIntensitiesAsync(IEnumerable dtos); - Task DeleteIntensityAsync(Guid resourceId); - - Task AddTestDataAsync(); + Task AddUpdateAllowedIntensityAsync(Guid detailTypeResourceId, IntensityDto intensityDto); + Task AddUpdateAllowedIntensitiesAsync(Guid detailTypeResourceId, IEnumerable intensityDtos); + Task RemoveAllowedIntensityAsync(Guid detailTypeResourceId, Guid intensityResourceId); + Task RemoveAllAllowedIntensitiesAsync(Guid detailTypeResourceId); } } \ No newline at end of file diff --git a/EventJournal.DomainService/UserTypesService.cs b/EventJournal.DomainService/UserTypesService.cs index c5f8ef8..6ae96f4 100644 --- a/EventJournal.DomainService/UserTypesService.cs +++ b/EventJournal.DomainService/UserTypesService.cs @@ -1,16 +1,13 @@ using AutoMapper; -using EventJournal.Data; -using EventJournal.Data.Entities; using EventJournal.Data.Entities.UserTypes; using EventJournal.Data.UserTypeRepositories; -using EventJournal.DomainDto; using EventJournal.DomainDto.UserTypes; +using EventJournal.DomainService.Exceptions; namespace EventJournal.DomainService { public class UserTypesService( - IEventTypeRepository eventTypeRepository, + IEventTypeRepository eventTypeRepository, IDetailTypeRepository detailTypeRepository, - IIntensityRepository intensityRepository, IMapper mapper) : IUserTypesService { @@ -27,16 +24,22 @@ public async Task> GetAllEventTypesAsync() { public async Task AddUpdateEventTypeAsync(EventTypeDto dto) { ArgumentNullException.ThrowIfNull(dto); //TODO: additional validations? - var eventTypeEntity = await eventTypeRepository.AddUpdateAsync(mapper.Map(dto)).ConfigureAwait(false); + var savedEntity = await AddUpdateEventTypePrivateAsync(mapper.Map(dto)).ConfigureAwait(false); await eventTypeRepository.SaveChangesAsync().ConfigureAwait(false); - return mapper.Map(eventTypeEntity); + return mapper.Map(savedEntity); + } + private Task AddUpdateEventTypePrivateAsync(EventType entity) { + return eventTypeRepository.AddUpdateAsync(entity); } - public async Task> AddUpdateEventTypesAsync(IEnumerable dtos) { ArgumentNullException.ThrowIfNull(dtos); - var eventTypeEntities = await eventTypeRepository.AddUpdateAsync(mapper.Map>(dtos)).ConfigureAwait(false); + List eventTypes = []; + foreach (var dto in dtos) { + EventType eventType = await AddUpdateEventTypePrivateAsync(mapper.Map(dto)).ConfigureAwait(false); + eventTypes.AddRange(eventType); + } await eventTypeRepository.SaveChangesAsync().ConfigureAwait(false); - return mapper.Map>(eventTypeEntities); + return mapper.Map>(eventTypes); } public async Task DeleteEventTypeAsync(Guid resourceId) { @@ -60,14 +63,25 @@ public async Task> GetAllDetailTypesAsync() { public async Task AddUpdateDetailTypeAsync(DetailTypeDto dto) { ArgumentNullException.ThrowIfNull(dto); //TODO: additional validations? - var entity = await detailTypeRepository.AddUpdateAsync(mapper.Map(dto)).ConfigureAwait(false); + var entity = await AddUpdateDetailTypePrivateAsync(mapper.Map(dto)).ConfigureAwait(false); await detailTypeRepository.SaveChangesAsync().ConfigureAwait(false); return mapper.Map(entity); } - + private Task AddUpdateDetailTypePrivateAsync(DetailType entity) { + // don't duplicate intensities - match on either Id or ResourceId + foreach (var intensity in entity.AllowedIntensities) { + intensity.DetailType = entity; + var existingIntensity = entity.AllowedIntensities.FirstOrDefault(i => i.Id == intensity.Id || i.ResourceId == intensity.ResourceId); + if (existingIntensity != null) { + intensity.Id = existingIntensity.Id; + intensity.ResourceId = existingIntensity.ResourceId; + } + } + return detailTypeRepository.AddUpdateAsync(entity); + } public async Task> AddUpdateDetailTypesAsync(IEnumerable dtos) { ArgumentNullException.ThrowIfNull(dtos); - var detailTypeEntities = await detailTypeRepository.AddUpdateAsync(mapper.Map>(dtos)).ConfigureAwait(false); + var detailTypeEntities = await AddUpdateDetailTypePrivateAsync(mapper.Map(dtos)).ConfigureAwait(false); await detailTypeRepository.SaveChangesAsync().ConfigureAwait(false); return mapper.Map>(detailTypeEntities); } @@ -81,43 +95,31 @@ public async Task DeleteDetailTypeAsync(Guid resourceId) { await detailTypeRepository.SaveChangesAsync().ConfigureAwait(false); } - // ======================> Intensities <====================== - public async Task> GetAllIntensitiesAsync() { - return mapper.Map>(await intensityRepository.GetAllAsync().ConfigureAwait(false)); - } - - public async Task GetIntensityByIdAsync(Guid resourceId) { - return mapper.Map(await intensityRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false)); - } - - public async Task AddUpdateIntensityAsync(IntensityDto dto) { - ArgumentNullException.ThrowIfNull(dto); - //TODO: additional validations? - var entity = await intensityRepository.AddUpdateAsync(mapper.Map(dto)).ConfigureAwait(false); - await intensityRepository.SaveChangesAsync().ConfigureAwait(false); - return mapper.Map(entity); + public async Task AddUpdateAllowedIntensityAsync(Guid detailTypeResourceId, IntensityDto intensityDto) { + ArgumentNullException.ThrowIfNull(intensityDto); + var detailTypeEntity = await detailTypeRepository.GetByResourceIdAsync(detailTypeResourceId).ConfigureAwait(false) ?? throw new ResourceNotFoundException($"DetailType with ResourceId {detailTypeResourceId} not found."); + var result = detailTypeEntity.AddUpdateAllowedIntensity(mapper.Map(intensityDto)); + await detailTypeRepository.SaveChangesAsync().ConfigureAwait(false); + return mapper.Map(result); } - public async Task> AddUpdateIntensitiesAsync(IEnumerable dtos) { - ArgumentNullException.ThrowIfNull(dtos); - var intensityEntities = await intensityRepository.AddUpdateAsync(mapper.Map>(dtos)).ConfigureAwait(false); - await intensityRepository.SaveChangesAsync().ConfigureAwait(false); - return mapper.Map>(intensityEntities); + public async Task AddUpdateAllowedIntensitiesAsync(Guid detailTypeResourceId, IEnumerable intensityDtos) { + ArgumentNullException.ThrowIfNull(intensityDtos); + var detailTypeEntity = await detailTypeRepository.GetByResourceIdAsync(detailTypeResourceId).ConfigureAwait(false) ?? throw new ResourceNotFoundException($"DetailType with ResourceId {detailTypeResourceId} not found."); + detailTypeEntity.AddUpdateAllowedIntensities(mapper.Map>(intensityDtos)); + await detailTypeRepository.SaveChangesAsync().ConfigureAwait(false); } - public async Task DeleteIntensityAsync(Guid resourceId) { - var entity = await intensityRepository.GetByResourceIdAsync(resourceId).ConfigureAwait(false); - if (entity == null) { - return; - } - intensityRepository.Delete(entity); - await intensityRepository.SaveChangesAsync().ConfigureAwait(false); + public async Task RemoveAllowedIntensityAsync(Guid detailTypeResourceId, Guid intensityResourceId) { + var detailTypeEntity = await detailTypeRepository.GetByResourceIdAsync(detailTypeResourceId).ConfigureAwait(false) ?? throw new ResourceNotFoundException($"DetailType with ResourceId {detailTypeResourceId} not found."); + detailTypeEntity.RemoveIntensity(intensityResourceId); + await detailTypeRepository.SaveChangesAsync().ConfigureAwait(false); } - public async Task AddTestDataAsync() { - await AddUpdateEventTypesAsync(EventTypeDto.DefaultEventTypeDtos).ConfigureAwait(false); - await AddUpdateDetailTypeAsync(DetailTypeDto.DefaultDetailTypeDto).ConfigureAwait(false); - await AddUpdateIntensitiesAsync(IntensityDto.DefaultIntensityDtos).ConfigureAwait(false); + public async Task RemoveAllAllowedIntensitiesAsync(Guid detailTypeResourceId) { + var detailTypeEntity = await detailTypeRepository.GetByResourceIdAsync(detailTypeResourceId).ConfigureAwait(false) ?? throw new ResourceNotFoundException($"DetailType with ResourceId {detailTypeResourceId} not found."); + detailTypeEntity.RemoveAllIntensities(); + await detailTypeRepository.SaveChangesAsync().ConfigureAwait(false); } } } diff --git a/README.md b/README.md index d36bf97..228bb01 100644 --- a/README.md +++ b/README.md @@ -62,16 +62,11 @@ EF https://learn.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs ### NEXT STEPS -- Fix repository structure - - ~~move details back to event service/repo and rename detail service to types service and move event types there~~ - - ~~we need a separate types service because types can be added/removed independently of events~~ - - ~~remove detail repository~~ and intensity repository - - only add/delete or even GET intensities via methods on detail type - - ~~likewise only add/delete or even GET details via methods on event~~ - fix default data (move from DTOs to repository so that individual entities are not duplicated when added via parents) - eg you don't get duplicate event type entity when event is saved - in other words the event type added when event is added isn't a duplicate + - this is still an issue! - ~~move default event and default details into event service~~ - - move all default user type data into user types service (follow event service's example for default data) + - ~~move all default user type data into user types service (follow event service's example for default data)~~ - test full get event with details - test full get detail type with allowed intensities - add multiple default detail types to initializer (a la event types) From 0f7fd8dd8c86e429474473c1fe5e3558f17e9e8c Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Sun, 7 Sep 2025 16:57:05 -0400 Subject: [PATCH 28/34] Update services to properly handle loading user defined types when saving Entities and Details Fix issue with saving allowed Intensities. --- EventJournal.DomainService/EventService.cs | 49 +++++++++++++------ .../IInternalUserTypeService.cs | 8 +++ .../UserTypesService.cs | 11 ++++- 3 files changed, 51 insertions(+), 17 deletions(-) create mode 100644 EventJournal.DomainService/IInternalUserTypeService.cs diff --git a/EventJournal.DomainService/EventService.cs b/EventJournal.DomainService/EventService.cs index afd0f38..62a3c44 100644 --- a/EventJournal.DomainService/EventService.cs +++ b/EventJournal.DomainService/EventService.cs @@ -1,6 +1,7 @@ using AutoMapper; using EventJournal.Data; using EventJournal.Data.Entities; +using EventJournal.Data.Entities.UserTypes; using EventJournal.DomainDto; using EventJournal.DomainService.Exceptions; @@ -11,7 +12,7 @@ public class EventService( IUserTypesService userTypesService, IMapper mapper) : IEventService { - + private readonly IInternalUserTypeService internalUserTypeService = userTypesService as IInternalUserTypeService ?? throw new InvalidOperationException("userTypesService must implement IInternalUserTypeService"); // ======================> Events <====================== public async Task> GetAllEventsAsync() { return mapper.Map>(await eventRepository.GetAllAsync().ConfigureAwait(false)); @@ -24,33 +25,36 @@ public async Task> GetAllEventsAsync() { public async Task AddUpdateEventAsync(EventDto dto) { ArgumentNullException.ThrowIfNull(dto); //TODO: additional validations? - var savedEntity = await AddUpdateEventPrivateAsync(mapper.Map(dto)).ConfigureAwait(false); - // so the problem is that we need to find existing DetailType with it's existing AllowedIntensities and THEN assign detail to detail - // and pick the right intensity. Not sure this should happen here maybe in AddUpdateDetailAsync() ?? - // - in other wrds extend the logic in AddUpdteEventPrivateAsync to utilize similar logic that exists in the USErTypesServie for DetailType and Intensities. - // /// remember the goal is that Detail selects a valid intensity from teh list of allowed intensities specified by detail type. - // another stray thought: maybe we need a mapping table that maps Detail id and Intensity id? + var eventTypeEntity = await internalUserTypeService.GetEventTypeEntityAsync(dto.EventType.ResourceId).ConfigureAwait(false) ?? throw new ResourceNotFoundException($"EventType with ResourceId {dto.EventType.ResourceId} not found."); + + var savedEvent = await AddUpdateEventPrivateAsync(mapper.Map(dto), eventTypeEntity); + await eventRepository.SaveChangesAsync().ConfigureAwait(false); - return mapper.Map(savedEntity); + return mapper.Map(savedEvent); } - private Task AddUpdateEventPrivateAsync(Event entity) { + + private Task AddUpdateEventPrivateAsync(Event @event, EventType eventType) { + ArgumentNullException.ThrowIfNull(eventType); // don't duplicate details - match on either Id or ResourceId - foreach (var detail in entity.Details) { - detail.Event = entity; - var existingDetail = entity.Details.FirstOrDefault(d => d.Id == detail.Id || d.ResourceId == detail.ResourceId); + @event.EventType = eventType; + foreach (var detail in @event.Details) { + detail.Event = @event; + var existingDetail = @event.Details.First(d => d.ResourceId == detail.ResourceId || (d.Id != 0 && d.Id == detail.Id)); if (existingDetail != null) { detail.Id = existingDetail.Id; detail.ResourceId = existingDetail.ResourceId; } } - return eventRepository.AddUpdateAsync(entity); + return eventRepository.AddUpdateAsync(@event); } public async Task> AddUpdateEventsAsync(IEnumerable dtos) { ArgumentNullException.ThrowIfNull(dtos); var events = new List(); foreach (var dto in dtos) { - Event @event = await AddUpdateEventPrivateAsync(mapper.Map(dto)).ConfigureAwait(false); + //TODO: additional validations? + var eventTypeEntity = await internalUserTypeService.GetEventTypeEntityAsync(dto.EventType.ResourceId).ConfigureAwait(false) ?? throw new ResourceNotFoundException($"EventType with ResourceId {dto.EventType.ResourceId} not found."); + Event @event = await AddUpdateEventPrivateAsync(mapper.Map(dto), eventTypeEntity).ConfigureAwait(false); events.Add(@event); } await eventRepository.SaveChangesAsync().ConfigureAwait(false); @@ -68,9 +72,24 @@ public async Task DeleteEventAsync(Guid resourceId) { } public async Task AddUpdateDetailAsync(Guid eventResourceId, DetailDto detailDto) { + + // so the problem is that we need to find existing DetailType with it's existing AllowedIntensities and THEN assign detail to detail + // and pick the right intensity. Not sure this should happen here maybe in AddUpdateDetailAsync() ?? + // - in other wrds extend the logic in AddUpdteEventPrivateAsync to utilize similar logic that exists in the USErTypesServie for DetailType and Intensities. + // /// remember the goal is that Detail selects a valid intensity from the list of allowed intensities specified by detail type. + // another stray thought: maybe we need a mapping table that maps Detail id and Intensity id? ArgumentNullException.ThrowIfNull(detailDto); var eventEntity = await eventRepository.GetByResourceIdAsync(eventResourceId).ConfigureAwait(false) ?? throw new ResourceNotFoundException($"Event with ResourceId {eventResourceId} not found."); - var result = eventEntity.AddUpdateDetail(mapper.Map(detailDto)); + + var detailTypeEntity = await internalUserTypeService.GetDetailTypeEntityAsync(detailDto.DetailType.ResourceId).ConfigureAwait(false) ?? throw new ResourceNotFoundException($"DetailType with ResourceId {detailDto.DetailType.ResourceId} not found."); + var intensityEntity = detailTypeEntity.AllowedIntensities.FirstOrDefault(i => i.ResourceId == detailDto.Intensity.ResourceId) ?? throw new ResourceNotFoundException($"Intensity with ResourceId {detailDto.Intensity.ResourceId} not found in DetailType with ResourceId {detailDto.DetailType.ResourceId}."); + + var detailEntity = mapper.Map(detailDto); + //TODO: move these to mapper when you replace AutoMapper + detailEntity.DetailType = detailTypeEntity; + detailEntity.Intensity = intensityEntity; + + var result = eventEntity.AddUpdateDetail(detailEntity); await eventRepository.SaveChangesAsync().ConfigureAwait(false); return mapper.Map(result); } diff --git a/EventJournal.DomainService/IInternalUserTypeService.cs b/EventJournal.DomainService/IInternalUserTypeService.cs new file mode 100644 index 0000000..9322ad9 --- /dev/null +++ b/EventJournal.DomainService/IInternalUserTypeService.cs @@ -0,0 +1,8 @@ +using EventJournal.Data.Entities.UserTypes; + +namespace EventJournal.DomainService { + internal interface IInternalUserTypeService : IUserTypesService { + Task GetEventTypeEntityAsync(Guid resourceId); + Task GetDetailTypeEntityAsync(Guid resourceId); + } +} diff --git a/EventJournal.DomainService/UserTypesService.cs b/EventJournal.DomainService/UserTypesService.cs index 6ae96f4..0d2f09a 100644 --- a/EventJournal.DomainService/UserTypesService.cs +++ b/EventJournal.DomainService/UserTypesService.cs @@ -9,7 +9,7 @@ public class UserTypesService( IEventTypeRepository eventTypeRepository, IDetailTypeRepository detailTypeRepository, IMapper mapper) - : IUserTypesService { + : IUserTypesService, IInternalUserTypeService { // ======================> Event Types <====================== @@ -51,6 +51,9 @@ public async Task DeleteEventTypeAsync(Guid resourceId) { await eventTypeRepository.SaveChangesAsync().ConfigureAwait(false); } + public Task GetEventTypeEntityAsync(Guid resourceId) { + return eventTypeRepository.GetByResourceIdAsync(resourceId); + } // ======================> Detail Types <====================== public async Task> GetAllDetailTypesAsync() { return mapper.Map>(await detailTypeRepository.GetAllAsync().ConfigureAwait(false)); @@ -71,7 +74,7 @@ private Task AddUpdateDetailTypePrivateAsync(DetailType entity) { // don't duplicate intensities - match on either Id or ResourceId foreach (var intensity in entity.AllowedIntensities) { intensity.DetailType = entity; - var existingIntensity = entity.AllowedIntensities.FirstOrDefault(i => i.Id == intensity.Id || i.ResourceId == intensity.ResourceId); + var existingIntensity = entity.AllowedIntensities.First(i => i.ResourceId == intensity.ResourceId || (i.Id != 0 && i.Id == intensity.Id)); if (existingIntensity != null) { intensity.Id = existingIntensity.Id; intensity.ResourceId = existingIntensity.ResourceId; @@ -121,5 +124,9 @@ public async Task RemoveAllAllowedIntensitiesAsync(Guid detailTypeResourceId) { detailTypeEntity.RemoveAllIntensities(); await detailTypeRepository.SaveChangesAsync().ConfigureAwait(false); } + + public Task GetDetailTypeEntityAsync(Guid resourceId) { + return detailTypeRepository.GetByResourceIdAsync(resourceId); + } } } From cedbf510825b4974171e41f9e1aad0cf19788ff4 Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Sun, 7 Sep 2025 22:42:04 -0400 Subject: [PATCH 29/34] Fix more issues with add/update data --- EventJournal.Data/Entities/EntityHelper.cs | 2 +- EventJournal.Data/Entities/Event.cs | 11 +- .../Entities/UserTypes/DetailType.cs | 15 +- .../DefaultDataProvider.cs | 181 ++++++++++++------ EventJournal.DomainService/EventService.cs | 35 ++-- .../UserTypesService.cs | 21 +- README.md | 45 +++-- 7 files changed, 192 insertions(+), 118 deletions(-) diff --git a/EventJournal.Data/Entities/EntityHelper.cs b/EventJournal.Data/Entities/EntityHelper.cs index 3834aed..7141ccf 100644 --- a/EventJournal.Data/Entities/EntityHelper.cs +++ b/EventJournal.Data/Entities/EntityHelper.cs @@ -5,7 +5,7 @@ public static T UpdateEntity(this T destination, T source) where T : BaseEnti ArgumentNullException.ThrowIfNull(source); if (destination.ResourceId != source.ResourceId) //TODO: custom exception that supports a ThrowIf parameter? - throw new InvalidOperationException("ResourceIds do not match"); + throw new InvalidOperationException($"{typeof(T).Name} ResourceIds do not match"); destination.CopyUserValues(source); destination.UpdatedDate = DateTime.UtcNow; return destination; diff --git a/EventJournal.Data/Entities/Event.cs b/EventJournal.Data/Entities/Event.cs index f344338..034f362 100644 --- a/EventJournal.Data/Entities/Event.cs +++ b/EventJournal.Data/Entities/Event.cs @@ -24,7 +24,7 @@ public class Event : BaseEntity { public Detail AddUpdateDetail(Detail detail) { ArgumentNullException.ThrowIfNull(detail, nameof(detail)); - var existingDetail = _details.FirstOrDefault(d => d.Id == detail.Id || d.ResourceId == detail.ResourceId); + var existingDetail = _details.FirstOrDefault(d => d.ResourceId == detail.ResourceId || (d.Id != 0 && d.Id == detail.Id)); if (existingDetail != null) { //TODO: shouldn't this already be the case? existingDetail.Event = this; @@ -35,13 +35,6 @@ public Detail AddUpdateDetail(Detail detail) { return detail; } - public void AddUpdateDetails(IEnumerable details) { - ArgumentNullException.ThrowIfNull(details, nameof(details)); - foreach (var detail in details) { - AddUpdateDetail(detail); - } - } - public void RemoveDetail(Guid detailResourceId) { var existingDetail = _details.FirstOrDefault(d => d.ResourceId == detailResourceId); if (existingDetail != null) { @@ -55,7 +48,7 @@ public void RemoveAllDetails() { internal override void CopyUserValues(T source) { var soruceEvent = source as Event ?? throw new InvalidCastException($"{nameof(source)} is not of type {typeof(Event)}"); - EventType.UpdateEntity(soruceEvent.EventType); + EventType = soruceEvent.EventType; StartTime = soruceEvent.StartTime; EndTime = soruceEvent.EndTime; Description = soruceEvent.Description; diff --git a/EventJournal.Data/Entities/UserTypes/DetailType.cs b/EventJournal.Data/Entities/UserTypes/DetailType.cs index b7fd136..aa4a6ce 100644 --- a/EventJournal.Data/Entities/UserTypes/DetailType.cs +++ b/EventJournal.Data/Entities/UserTypes/DetailType.cs @@ -21,11 +21,11 @@ public class DetailType : BaseEntity { public SortType IntensitySortType { get; set; } = SortType.None; public IReadOnlyCollection AllowedIntensities => (IReadOnlyCollection)_intensities; - private IList _intensities = []; + private readonly IList _intensities = []; public Intensity AddUpdateAllowedIntensity(Intensity intensity) { ArgumentNullException.ThrowIfNull(intensity, nameof(intensity)); - var existingIntensity = _intensities.FirstOrDefault(i => i.Id == intensity.Id || i.ResourceId == intensity.ResourceId); + var existingIntensity = _intensities.FirstOrDefault(i => i.ResourceId == intensity.ResourceId || (i.Id != 0 && i.Id == intensity.Id)); if (existingIntensity != null) { //TODO: shouldn't this already be the case? existingIntensity.DetailType = this; @@ -36,15 +36,8 @@ public Intensity AddUpdateAllowedIntensity(Intensity intensity) { return intensity; } - public void AddUpdateAllowedIntensities(IEnumerable intensities) { - ArgumentNullException.ThrowIfNull(intensities, nameof(intensities)); - foreach (var intensity in intensities) { - AddUpdateAllowedIntensity(intensity); - } - } - - public void RemoveIntensity(Guid intensityResoruceId) { - var existignIntensity = _intensities.FirstOrDefault(i => i.ResourceId == intensityResoruceId); + public void RemoveIntensity(Guid intensityResourceId) { + var existignIntensity = _intensities.FirstOrDefault(i => i.ResourceId == intensityResourceId); if (existignIntensity != null) { _intensities.Remove(existignIntensity); } diff --git a/EventJournal.DomainService/DefaultDataProvider.cs b/EventJournal.DomainService/DefaultDataProvider.cs index ed81d7d..5404228 100644 --- a/EventJournal.DomainService/DefaultDataProvider.cs +++ b/EventJournal.DomainService/DefaultDataProvider.cs @@ -8,7 +8,7 @@ public class DefaultDataProvider( IUserTypesService userTypesService) : IDefaultDataProvider { private static readonly Guid DefaultEventResourceId = Guid.Parse("00000000-0000-0000-0000-000000000001"); - private static readonly Guid DefaultDetailResourceId = Guid.Parse("00000000-0000-0000-0000-000000000001"); + private static readonly Guid[] DefaultEventTypeResourceIds = [ Guid.Parse("00000000-0000-0000-0000-000000000001"), Guid.Parse("00000000-0000-0000-0000-000000000002"), @@ -16,15 +16,11 @@ public class DefaultDataProvider( Guid.Parse("00000000-0000-0000-0000-000000000004"), Guid.Parse("00000000-0000-0000-0000-000000000005") ]; - private static readonly Guid DefaultDetailTypeResourceId = Guid.Parse("00000000-0000-0000-0000-000000000001"); - - private static readonly Guid[] DefaultIntensityResourceIds = [ + private static readonly Guid[] DefaultDetailTypeResourceIds = [ Guid.Parse("00000000-0000-0000-0000-000000000001"), Guid.Parse("00000000-0000-0000-0000-000000000002"), - Guid.Parse("00000000-0000-0000-0000-000000000003"), - Guid.Parse("00000000-0000-0000-0000-000000000004"), - Guid.Parse("00000000-0000-0000-0000-000000000005"), - ]; + Guid.Parse("00000000-0000-0000-0000-000000000003") + ]; public async Task AddResetDefaultDataAsync() { var defaultEventTypes = await userTypesService.AddUpdateEventTypesAsync([ @@ -35,59 +31,128 @@ public async Task AddResetDefaultDataAsync() { new EventTypeDto{ ResourceId = DefaultEventTypeResourceIds[4], Name = "Exercise", Description=""} ]).ConfigureAwait(false); - var defaultDetailTypeDto = await userTypesService.AddUpdateDetailTypeAsync(new DetailTypeDto { - ResourceId = DefaultDetailTypeResourceId, - Description = "This is a generic detail type", - Name = "Generic Detail Type", - IntensitySortType = SortType.Descending, - AllowedIntensities = [ - new() { - ResourceId = DefaultIntensityResourceIds[0], - Level = 0, - Name = "Zero Intensity", - Description = "Not intense at all. Normal." - }, - new() { - ResourceId = DefaultIntensityResourceIds[1], - Level = 1, - Name = "Mild Intensity", - Description = "Almost broke a sweat." - }, - new() { - ResourceId = DefaultIntensityResourceIds[2], - Level = 2, - Name = "Moderate Intensity", - Description = "Got sweaty, did some breathing. Might feel this tomorrow." - }, - new() { - ResourceId = DefaultIntensityResourceIds[3], - Level = 3, - Name = "High Intensity", - Description = "Breathing hard. Will definitely feel this for a few days." - }, - new() { - ResourceId = DefaultIntensityResourceIds[4], - Level = 4, - Name = "Insane Intensity", - Description = "Unbearable. Insane. No one needs to experience this. Why did I do this to myself?" - } - ] - }).ConfigureAwait(false); + IEnumerable defaultDetailTypeDtos = await userTypesService.AddUpdateDetailTypesAsync([ + new DetailTypeDto { + ResourceId = DefaultDetailTypeResourceIds[1], + Description = "This is a generic detail type", + Name = "Generic", + IntensitySortType = SortType.Descending, + AllowedIntensities = [ + new IntensityDto { + ResourceId = Guid.NewGuid(), + Level = 0, + Name = "Zero Intensity", + Description = "Not intense at all. Normal." + }, + new IntensityDto { + ResourceId = Guid.NewGuid(), + Level = 1, + Name = "Mild Intensity", + Description = "Almost broke a sweat." + }, + new IntensityDto { + ResourceId = Guid.NewGuid(), + Level = 2, + Name = "Moderate Intensity", + Description = "Got sweaty, did some breathing. Might feel this tomorrow." + }, + new IntensityDto { + ResourceId = Guid.NewGuid(), + Level = 3, + Name = "High Intensity", + Description = "Breathing hard. Will definitely feel this for a few days." + }, + new IntensityDto { + ResourceId = Guid.NewGuid(), + Level = 4, + Name = "Insane Intensity", + Description = "Unbearable. Insane. No one needs to experience this. Why did I do this to myself?" + } + ] + }, + //TODO: breakfast seems a bit too specific for a default detail type but it's an example of how to use detail types and intensities + // Consider changing to something more generic like "Meal" or "Food Intake" and leaving the specifics to the Detail Notes + new DetailTypeDto { + ResourceId = DefaultDetailTypeResourceIds[2], + Description = "Use this detail type to track your breakfast.", + Name = "Meal", + IntensitySortType = SortType.Descending, + AllowedIntensities = [ + new IntensityDto { + ResourceId = Guid.NewGuid(), + Level = 0, + Name = "Skipped", + Description = "Skipped altogether or just had coffee, tea, or a soda." + }, + new IntensityDto { + ResourceId = Guid.NewGuid(), + Level = 1, + Name = "Unhealthy", + Description = "Ate something but it wasn't really a healthy choice." + }, + new IntensityDto { + ResourceId = Guid.NewGuid(), + Level = 2, + Name = "Moderately Healthy", + Description = "Made a moderately healthy choice." + }, + new IntensityDto { + ResourceId = Guid.NewGuid(), + Level = 3, + Name = "Power Meal", + Description = "Made a very healthy choice." + } + ] + } + ]).ConfigureAwait(false); + var eventDtos = await eventService.AddUpdateEventsAsync([ + new EventDto { + ResourceId = DefaultEventResourceId, + StartTime = DateTime.Now, + Description = "Event History Started", + EventType = defaultEventTypes.First(d => d.Name == "Random Event") + }, + new EventDto { + ResourceId = Guid.NewGuid(), + StartTime = DateTime.Now.AddHours(-1), + EndTime = DateTime.Now.AddHours(-1).AddMinutes(30), + Description = "Ate breakfast", + EventType = defaultEventTypes.First(et => et.Name == "Ate Something") + } + ]); - var eventDto = await eventService.AddUpdateEventAsync(new EventDto { - ResourceId = DefaultEventResourceId, - StartTime = DateTime.Now, - Description = "Event History Started", - EventType = defaultEventTypes.First() - }); + var genericDetailType = defaultDetailTypeDtos.First(dt => dt.Name == "Generic"); + var mealDetailType = defaultDetailTypeDtos.First(dt => dt.Name == "Meal"); + await eventService.AddUpdateDetailsAsync(eventDtos.First().ResourceId, [ + new DetailDto { + ResourceId = Guid.NewGuid(), + DetailType = genericDetailType, + Intensity = genericDetailType.AllowedIntensities.First(i => i.Level == 2), + Notes = "Congratulations on starting your Event History!\nTake the next step and add another event!" + }, + new DetailDto { + ResourceId = Guid.NewGuid(), + DetailType = mealDetailType, + Intensity = mealDetailType.AllowedIntensities.First(i => i.Level == 1), + Notes = "This is an example of how you can use details to track more information about your events.\nYou can add multiple details to an event." + } + ]); - await eventService.AddUpdateDetailAsync(eventDto.ResourceId, new DetailDto { - ResourceId = DefaultDetailResourceId, - DetailType = defaultDetailTypeDto, - Intensity = defaultDetailTypeDto.AllowedIntensities.First(), - Notes = "Congratulations on starting your Event History!\nTake the next step and add another event!" - }); + await eventService.AddUpdateDetailsAsync(eventDtos.ElementAt(1).ResourceId, [ + new DetailDto { + ResourceId = Guid.NewGuid(), + DetailType = mealDetailType, + Intensity = mealDetailType.AllowedIntensities.First(i => i.Level == 0), + Notes = "Was running late - Just had coffee." + }, + new DetailDto { + ResourceId = Guid.NewGuid(), + DetailType = mealDetailType, + Intensity = mealDetailType.AllowedIntensities.First(i => i.Level == 3), + Notes = "Realized I had time to stop at the cafe and grab Eggs and Fruit" + } + ]); } } diff --git a/EventJournal.DomainService/EventService.cs b/EventJournal.DomainService/EventService.cs index 62a3c44..ca4b2a3 100644 --- a/EventJournal.DomainService/EventService.cs +++ b/EventJournal.DomainService/EventService.cs @@ -39,7 +39,7 @@ private Task AddUpdateEventPrivateAsync(Event @event, EventType eventType @event.EventType = eventType; foreach (var detail in @event.Details) { detail.Event = @event; - var existingDetail = @event.Details.First(d => d.ResourceId == detail.ResourceId || (d.Id != 0 && d.Id == detail.Id)); + var existingDetail = @event.Details.First(d => d.ResourceId == detail.ResourceId || (d.Id != 0 && d.Id == detail.Id)); if (existingDetail != null) { detail.Id = existingDetail.Id; detail.ResourceId = existingDetail.ResourceId; @@ -72,32 +72,25 @@ public async Task DeleteEventAsync(Guid resourceId) { } public async Task AddUpdateDetailAsync(Guid eventResourceId, DetailDto detailDto) { - - // so the problem is that we need to find existing DetailType with it's existing AllowedIntensities and THEN assign detail to detail - // and pick the right intensity. Not sure this should happen here maybe in AddUpdateDetailAsync() ?? - // - in other wrds extend the logic in AddUpdteEventPrivateAsync to utilize similar logic that exists in the USErTypesServie for DetailType and Intensities. - // /// remember the goal is that Detail selects a valid intensity from the list of allowed intensities specified by detail type. - // another stray thought: maybe we need a mapping table that maps Detail id and Intensity id? ArgumentNullException.ThrowIfNull(detailDto); - var eventEntity = await eventRepository.GetByResourceIdAsync(eventResourceId).ConfigureAwait(false) ?? throw new ResourceNotFoundException($"Event with ResourceId {eventResourceId} not found."); - - var detailTypeEntity = await internalUserTypeService.GetDetailTypeEntityAsync(detailDto.DetailType.ResourceId).ConfigureAwait(false) ?? throw new ResourceNotFoundException($"DetailType with ResourceId {detailDto.DetailType.ResourceId} not found."); - var intensityEntity = detailTypeEntity.AllowedIntensities.FirstOrDefault(i => i.ResourceId == detailDto.Intensity.ResourceId) ?? throw new ResourceNotFoundException($"Intensity with ResourceId {detailDto.Intensity.ResourceId} not found in DetailType with ResourceId {detailDto.DetailType.ResourceId}."); - - var detailEntity = mapper.Map(detailDto); - //TODO: move these to mapper when you replace AutoMapper - detailEntity.DetailType = detailTypeEntity; - detailEntity.Intensity = intensityEntity; - var result = eventEntity.AddUpdateDetail(detailEntity); + var result = AddUpdateDetailPrivateAsync(eventResourceId, mapper.Map(detailDto)); await eventRepository.SaveChangesAsync().ConfigureAwait(false); return mapper.Map(result); } - - public async Task AddUpdateDetailsAsync(Guid eventResourceId, IEnumerable detailDtos) { - ArgumentNullException.ThrowIfNull(detailDtos); + private async Task AddUpdateDetailPrivateAsync(Guid eventResourceId, Detail detail) { + ArgumentNullException.ThrowIfNull(detail); var eventEntity = await eventRepository.GetByResourceIdAsync(eventResourceId).ConfigureAwait(false) ?? throw new ResourceNotFoundException($"Event with ResourceId {eventResourceId} not found."); - eventEntity.AddUpdateDetails(mapper.Map>(detailDtos)); + var detailTypeEntity = await internalUserTypeService.GetDetailTypeEntityAsync(detail.DetailType.ResourceId).ConfigureAwait(false) ?? throw new ResourceNotFoundException($"DetailType with ResourceId {detail.DetailType.ResourceId} not found."); + var intensityEntity = detailTypeEntity.AllowedIntensities.FirstOrDefault(i => i.ResourceId == detail.Intensity.ResourceId) ?? throw new ResourceNotFoundException($"Intensity with ResourceId {detail.Intensity.ResourceId} not found in DetailType with ResourceId {detail.DetailType.ResourceId}."); + detail.DetailType = detailTypeEntity; + detail.Intensity = intensityEntity; + return eventEntity.AddUpdateDetail(detail); + } + public async Task AddUpdateDetailsAsync(Guid eventResourceId, IEnumerable detailDtos) { + foreach (var detailDto in detailDtos) { + await AddUpdateDetailPrivateAsync(eventResourceId, mapper.Map(detailDto)).ConfigureAwait(false); + } await eventRepository.SaveChangesAsync().ConfigureAwait(false); } diff --git a/EventJournal.DomainService/UserTypesService.cs b/EventJournal.DomainService/UserTypesService.cs index 0d2f09a..f0cfd37 100644 --- a/EventJournal.DomainService/UserTypesService.cs +++ b/EventJournal.DomainService/UserTypesService.cs @@ -84,7 +84,10 @@ private Task AddUpdateDetailTypePrivateAsync(DetailType entity) { } public async Task> AddUpdateDetailTypesAsync(IEnumerable dtos) { ArgumentNullException.ThrowIfNull(dtos); - var detailTypeEntities = await AddUpdateDetailTypePrivateAsync(mapper.Map(dtos)).ConfigureAwait(false); + List detailTypeEntities = []; + foreach (var dto in dtos) { + detailTypeEntities.Add( await AddUpdateDetailTypePrivateAsync(mapper.Map(dto)).ConfigureAwait(false)); + } await detailTypeRepository.SaveChangesAsync().ConfigureAwait(false); return mapper.Map>(detailTypeEntities); } @@ -100,16 +103,20 @@ public async Task DeleteDetailTypeAsync(Guid resourceId) { public async Task AddUpdateAllowedIntensityAsync(Guid detailTypeResourceId, IntensityDto intensityDto) { ArgumentNullException.ThrowIfNull(intensityDto); - var detailTypeEntity = await detailTypeRepository.GetByResourceIdAsync(detailTypeResourceId).ConfigureAwait(false) ?? throw new ResourceNotFoundException($"DetailType with ResourceId {detailTypeResourceId} not found."); - var result = detailTypeEntity.AddUpdateAllowedIntensity(mapper.Map(intensityDto)); + var result = AddUpdateAllowedIntensityPrivateAsync(detailTypeResourceId, mapper.Map(intensityDto)); await detailTypeRepository.SaveChangesAsync().ConfigureAwait(false); return mapper.Map(result); } - - public async Task AddUpdateAllowedIntensitiesAsync(Guid detailTypeResourceId, IEnumerable intensityDtos) { - ArgumentNullException.ThrowIfNull(intensityDtos); + private async Task AddUpdateAllowedIntensityPrivateAsync(Guid detailTypeResourceId, Intensity intensity) { var detailTypeEntity = await detailTypeRepository.GetByResourceIdAsync(detailTypeResourceId).ConfigureAwait(false) ?? throw new ResourceNotFoundException($"DetailType with ResourceId {detailTypeResourceId} not found."); - detailTypeEntity.AddUpdateAllowedIntensities(mapper.Map>(intensityDtos)); + var result = detailTypeEntity.AddUpdateAllowedIntensity(intensity); + await detailTypeRepository.SaveChangesAsync().ConfigureAwait(false); + return result; + } + public async Task AddUpdateAllowedIntensitiesAsync(Guid detailTypeResourceId, IEnumerable intensityDtos) { + foreach (var intensityDto in intensityDtos) { + await AddUpdateAllowedIntensityPrivateAsync(detailTypeResourceId, mapper.Map(intensityDto)).ConfigureAwait(false); + } await detailTypeRepository.SaveChangesAsync().ConfigureAwait(false); } diff --git a/README.md b/README.md index 228bb01..97de74b 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ The concept of Intensity is still a bit muddy, but the idea is that events of a - [Exercise Intensity could be](#exercise-intensity-could-be) - [Entity Framework help](#entity-framework-help) - [NEXT STEPS](#next-steps) - - [TODO](#todo) - - [Future Considerations](#future-considerations) + - [re not n](#re-not-n) + - [an id) and have journal e](#an-id-and-have-journal-e) ## Features @@ -61,22 +61,45 @@ EF https://learn.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs ### NEXT STEPS +- add bootstrapper to setup + - setup dependency injection + - default data seeding (mvoe default data provider to bootstrapper?) + - move test data from defaultdataprovider to be inserted from UI + - (use this as an opportunity to add add methods to cli) +- still need to test adding event with details at the same time +- unit tests for + - repositories + - entity methods + - entity validators (if added) + - services + - mappers? + - dto helpers? + - bootstrapper + - other helpers/extensions? + - ?? web api controllers -- fix default data (move from DTOs to repository so that individual entities are not duplicated when added via parents) - - eg you don't get duplicate event type entity when event is saved - in other words the event type added when event is added isn't a duplicate - - this is still an issue! - - ~~move default event and default details into event service~~ - - ~~move all default user type data into user types service (follow event service's example for default data)~~ - test full get event with details - test full get detail type with allowed intensities -- add multiple default detail types to initializer (a la event types) +- add endpoints for: + - get all event types + - get all detail types with allowed intensities + - get all events (with details) + - get event by resource id (with details) + - get event type by resource id + - get detail type by resource id (with allowed intensities) + - add event type + - add detail type (with allowed intensities) + - add event (with details) + - update event type + - update detail type (with allowed intensities) + - update event (with details) ### TODO -- unit tests for repositories and services. Also base entity code? -- Web API to call service methods +- Replace autoMapper with manual mapping +- add swagger to web api project - Add common BootStrap code to be consumed by cli and web api projects - remove microsoft.extension.hosting pkg where not needed -- move initializer code to an appropriate place +- move initializer code to an appropriate place (still needed?) - want to let user get some defaults to start with - but also wan to use these defaults for testing - Entity validators From f571b780d10232205a142b87f2d1d421ea7b4224 Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Sun, 5 Oct 2025 16:45:52 -0400 Subject: [PATCH 30/34] Add Bootstrap WIP --- EventJournal.CLI/EventJournal.CLI.csproj | 5 +- EventJournal.CLI/Program.cs | 12 +- EventJournal.Common/Bootstrap/BootStrapper.cs | 104 ++++++++++++++++++ EventJournal.Common/Bootstrap/IInstaller.cs | 8 ++ .../Bootstrap/ServiceCollectionExtensions.cs | 38 +++++++ .../EventJournal.Common.csproj | 18 +++ EventJournal.Common/IOC/DI.cs | 23 ++++ EventJournal.Data/Entities/EntityHelper.cs | 4 +- EventJournal.Data/EventJournal.Data.csproj | 4 +- EventJournal.DomainModels/DtoHelper.cs | 4 +- .../DefaultDataProvider.cs | 2 - EventJournal.DomainService/EventService.cs | 10 +- .../UserTypesService.cs | 10 +- .../EventJournal.PublicModels.csproj | 11 ++ .../EventJournal.WebAPI.csproj | 7 +- EventJournal.sln | 6 + .../DefaultApplicationBootStrapper.cs | 14 +++ .../EventJournal.BootStrap.csproj | 26 +++++ .../Installers/DistributedLockInstaller.cs | 16 +++ .../Installers/DomainServiceInstaller.cs | 13 +++ .../Installers/MiniProfilerInstaller.cs | 20 ++++ .../Installers/ModelMapperInstaller.cs | 12 ++ .../Installers/RepositoryInstaller.cs | 13 +++ README.md | 8 +- 24 files changed, 356 insertions(+), 32 deletions(-) create mode 100644 EventJournal.Common/Bootstrap/BootStrapper.cs create mode 100644 EventJournal.Common/Bootstrap/IInstaller.cs create mode 100644 EventJournal.Common/Bootstrap/ServiceCollectionExtensions.cs create mode 100644 EventJournal.Common/IOC/DI.cs create mode 100644 Eventjournal.Bootstrap/DefaultApplicationBootStrapper.cs create mode 100644 Eventjournal.Bootstrap/EventJournal.BootStrap.csproj create mode 100644 Eventjournal.Bootstrap/Installers/DistributedLockInstaller.cs create mode 100644 Eventjournal.Bootstrap/Installers/DomainServiceInstaller.cs create mode 100644 Eventjournal.Bootstrap/Installers/MiniProfilerInstaller.cs create mode 100644 Eventjournal.Bootstrap/Installers/ModelMapperInstaller.cs create mode 100644 Eventjournal.Bootstrap/Installers/RepositoryInstaller.cs diff --git a/EventJournal.CLI/EventJournal.CLI.csproj b/EventJournal.CLI/EventJournal.CLI.csproj index 368bebc..90a5a71 100644 --- a/EventJournal.CLI/EventJournal.CLI.csproj +++ b/EventJournal.CLI/EventJournal.CLI.csproj @@ -13,12 +13,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -26,6 +24,7 @@ + diff --git a/EventJournal.CLI/Program.cs b/EventJournal.CLI/Program.cs index 706c9a5..c80dc27 100644 --- a/EventJournal.CLI/Program.cs +++ b/EventJournal.CLI/Program.cs @@ -1,4 +1,5 @@ // See https://aka.ms/new-console-template for more information +using EventJournal.BootStrap; using EventJournal.CLI; using EventJournal.Data; using EventJournal.Data.UserTypeRepositories; @@ -6,6 +7,7 @@ using EventJournal.DomainService; using EventJournal.PublicModels; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System.Text.Json; @@ -27,7 +29,7 @@ private static async Task Main(string[] args) { //Console.WriteLine("Type '8' to "); Console.WriteLine("Type 'v' to view all data"); - Console.WriteLine("Type 'a' to add some test data."); + Console.WriteLine("Type 't' to add some test data."); Console.WriteLine("Type 'x' to delete all data."); Console.WriteLine("Type 'q' to quit."); @@ -66,7 +68,7 @@ private static async Task Main(string[] args) { case 'v': await ViewallDataAsync(GetSerializerOptions(), eventService, userTypeService).ConfigureAwait(false); break; - case 'a': + case 't': await AddTestDataAsync(defaultDataProvider).ConfigureAwait(false); break; case 'x': @@ -98,7 +100,11 @@ static IServiceProvider CreateServiceCollection() { options.ColorBehavior = Microsoft.Extensions.Logging.Console.LoggerColorBehavior.Enabled; }); }); - + // setup and register boostrapper and it's installers -- needs to be last + servicecollection.AddBootStrapper(Configuration, o => { + o.AddInstaller(new DomainServiceInstaller()); + o.AddInstaller(new IdentityServerInstaller()); + }); return servicecollection.BuildServiceProvider(); } static Task AddTestDataAsync(IDefaultDataProvider defaultDataProvider) { diff --git a/EventJournal.Common/Bootstrap/BootStrapper.cs b/EventJournal.Common/Bootstrap/BootStrapper.cs new file mode 100644 index 0000000..00ca606 --- /dev/null +++ b/EventJournal.Common/Bootstrap/BootStrapper.cs @@ -0,0 +1,104 @@ +using EventJournal.Common.IOC; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace EventJournal.Common.Bootstrap { + public class BootStrapper { + protected IList installers; + + public BootStrapper() { + installers = []; + } + + public virtual void AddInstaller(IInstaller installer) { + ArgumentNullException.ThrowIfNull(installer, nameof(installer)); + installers.Add(installer); + } + /// + /// Installs all of the specified installers, overriding the internal list of installers. + /// + /// A list of installers to register with the IoC. + public virtual IServiceProvider InitIoCContainer(params IInstaller[] installers) { + if (installers == null) { + throw new ArgumentNullException(nameof(installers), "Installers cannot be null"); + } + IServiceProvider container = InternalInitialize(installers); + return container; + } + + /// + /// Installs all the internally specified installers, while adding the [applicationInstaller] + /// + /// The additional installer for the root level application. + public virtual IServiceProvider InitIoCContainer(IInstaller applicationInstaller) { + if (applicationInstaller == null) { + throw new ArgumentNullException(nameof(applicationInstaller), "Application installer cannot be null"); + } + installers.Add(applicationInstaller); + return InternalInitialize([.. installers]); + } + + public virtual IServiceProvider InitIoCContainer() { + return InternalInitialize([.. installers]); + } + + public virtual IServiceProvider InitIoCContainer(IServiceCollection services) { + if (services == null) { + throw new ArgumentNullException(nameof(services), "Service collection cannot be null"); + } + return InternalInitialize(services, [.. installers]); + } + + public virtual IServiceProvider InitIoCContainer(IConfigurationBuilder config, IServiceCollection services) { + if (config == null) { + throw new ArgumentNullException(nameof(config), "Configuration builder cannot be null"); + } + return InternalInitialize(config, services, [.. installers]); + } + public virtual IServiceProvider InitIoCContainer(IConfiguration configuration, IServiceCollection services) { + if (configuration == null) { + throw new ArgumentNullException(nameof(configuration), "Configuration cannot be null"); + } + if (services == null) { + throw new ArgumentNullException(nameof(services), "Service collection cannot be null"); + } + return InternalInitialize(configuration, services, [.. installers]); + } + + protected internal virtual IServiceProvider InternalInitialize(IInstaller[] installers) { + var services = new ServiceCollection().AddOptions(); + return InternalInitialize(services, installers); + } + + protected internal virtual IServiceProvider InternalInitialize(IConfigurationBuilder config, IInstaller[] installers) { + var services = new ServiceCollection().AddOptions(); + return InternalInitialize(config, services, installers); + } + + protected internal virtual IServiceProvider InternalInitialize(IServiceCollection services, IInstaller[] installers) { + var configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json"); + return InternalInitialize(configuration, services, installers); + } + + protected internal virtual IServiceProvider InternalInitialize(IConfigurationBuilder config, IServiceCollection services, IInstaller[] installers) { + var configuration = config.Build(); + return InternalInitialize(configuration, services, installers); + } + + protected internal virtual IServiceProvider InternalInitialize(IConfiguration configuration, IServiceCollection services, IInstaller[] installers) { + DI.SetConfiguration(configuration); + + foreach (var i in installers) { + i.Install(services, configuration); + } + + services.AddSingleton(configuration); + var serviceProvider = services.BuildServiceProvider(); + + DI.SetContainer(serviceProvider); + return serviceProvider; + } + } + +} diff --git a/EventJournal.Common/Bootstrap/IInstaller.cs b/EventJournal.Common/Bootstrap/IInstaller.cs new file mode 100644 index 0000000..d291045 --- /dev/null +++ b/EventJournal.Common/Bootstrap/IInstaller.cs @@ -0,0 +1,8 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace EventJournal.Common.Bootstrap { + public interface IInstaller { + void Install(IServiceCollection services, IConfiguration configuration); + } +} diff --git a/EventJournal.Common/Bootstrap/ServiceCollectionExtensions.cs b/EventJournal.Common/Bootstrap/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..87d6854 --- /dev/null +++ b/EventJournal.Common/Bootstrap/ServiceCollectionExtensions.cs @@ -0,0 +1,38 @@ +using Microsoft.Extensions.DependencyInjection; +using System.Reflection; + +namespace EventJournal.Common.Bootstrap { + public static class ServiceCollectionExtensions { + /// + /// Add scoped classes with specified suffix from assembly that contains T + /// + /// + /// + /// + /// + public static IServiceCollection AddScopedInterfacesBySuffix(this IServiceCollection services, string suffix) where T : class { + typeof(T).GetTypeInfo().Assembly.GetTypes() + .Where(x => x.Name.EndsWith(suffix, StringComparison.InvariantCulture) + && x.GetTypeInfo().IsClass + && !x.GetTypeInfo().IsAbstract + && x.GetInterfaces().Length > 0) + .ToList().ForEach(x => { + x.GetInterfaces().ToList() + .ForEach(i => services.AddScoped(i, x)); + }); + + return services; + } + + public static IServiceCollection AddSingletonClassesBySuffix(this IServiceCollection services, string suffix) where T : class { + typeof(T).GetTypeInfo().Assembly.GetTypes() + .Where(x => x.Name.EndsWith(suffix, StringComparison.InvariantCulture) + && x.GetTypeInfo().IsClass + && !x.GetTypeInfo().IsAbstract) + .ToList() + .ForEach(x => services.AddSingleton(x)); + + return services; + } + } +} diff --git a/EventJournal.Common/EventJournal.Common.csproj b/EventJournal.Common/EventJournal.Common.csproj index 125f4c9..a8d35e3 100644 --- a/EventJournal.Common/EventJournal.Common.csproj +++ b/EventJournal.Common/EventJournal.Common.csproj @@ -6,4 +6,22 @@ enable + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/EventJournal.Common/IOC/DI.cs b/EventJournal.Common/IOC/DI.cs new file mode 100644 index 0000000..71cf0b9 --- /dev/null +++ b/EventJournal.Common/IOC/DI.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.Configuration; + +namespace EventJournal.Common.IOC { + public static class DI { + private static readonly object lockObject = new(); + + public static void SetContainer(IServiceProvider instance) { + lock (lockObject) { + Container = instance; + } + } + + public static IServiceProvider? Container { get; private set; } + + public static void SetConfiguration(IConfiguration instance) { + lock (lockObject) { + Configuration = instance; + } + } + + public static IConfiguration? Configuration { get; private set; } + } +} diff --git a/EventJournal.Data/Entities/EntityHelper.cs b/EventJournal.Data/Entities/EntityHelper.cs index 7141ccf..0d4a1e4 100644 --- a/EventJournal.Data/Entities/EntityHelper.cs +++ b/EventJournal.Data/Entities/EntityHelper.cs @@ -1,8 +1,8 @@ namespace EventJournal.Data.Entities { public static class EntityHelper { public static T UpdateEntity(this T destination, T source) where T : BaseEntity { - ArgumentNullException.ThrowIfNull(destination); - ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(destination, nameof(destination)); + ArgumentNullException.ThrowIfNull(source, nameof(source)); if (destination.ResourceId != source.ResourceId) //TODO: custom exception that supports a ThrowIf parameter? throw new InvalidOperationException($"{typeof(T).Name} ResourceIds do not match"); diff --git a/EventJournal.Data/EventJournal.Data.csproj b/EventJournal.Data/EventJournal.Data.csproj index b07aa8c..d14dc6e 100644 --- a/EventJournal.Data/EventJournal.Data.csproj +++ b/EventJournal.Data/EventJournal.Data.csproj @@ -11,11 +11,11 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/EventJournal.DomainModels/DtoHelper.cs b/EventJournal.DomainModels/DtoHelper.cs index cdc369c..aca7310 100644 --- a/EventJournal.DomainModels/DtoHelper.cs +++ b/EventJournal.DomainModels/DtoHelper.cs @@ -5,8 +5,8 @@ namespace EventJournal.DomainDto { public static partial class DtoHelper { //TODO: consider using AutoMapper for this if it is even needed public static T UpdateDTO(this T destination, T source) where T : BaseDto { - ArgumentNullException.ThrowIfNull(destination); - ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(destination, nameof(destination)); + ArgumentNullException.ThrowIfNull(source, nameof(source)); if (destination.ResourceId != source.ResourceId) //TODO: custom exception that supports a ThrowIf parameter? throw new InvalidOperationException("ResourceIds do not match"); diff --git a/EventJournal.DomainService/DefaultDataProvider.cs b/EventJournal.DomainService/DefaultDataProvider.cs index 5404228..bdf8a55 100644 --- a/EventJournal.DomainService/DefaultDataProvider.cs +++ b/EventJournal.DomainService/DefaultDataProvider.cs @@ -70,8 +70,6 @@ public async Task AddResetDefaultDataAsync() { } ] }, - //TODO: breakfast seems a bit too specific for a default detail type but it's an example of how to use detail types and intensities - // Consider changing to something more generic like "Meal" or "Food Intake" and leaving the specifics to the Detail Notes new DetailTypeDto { ResourceId = DefaultDetailTypeResourceIds[2], Description = "Use this detail type to track your breakfast.", diff --git a/EventJournal.DomainService/EventService.cs b/EventJournal.DomainService/EventService.cs index ca4b2a3..54f52a6 100644 --- a/EventJournal.DomainService/EventService.cs +++ b/EventJournal.DomainService/EventService.cs @@ -23,7 +23,7 @@ public async Task> GetAllEventsAsync() { } public async Task AddUpdateEventAsync(EventDto dto) { - ArgumentNullException.ThrowIfNull(dto); + ArgumentNullException.ThrowIfNull(dto, nameof(dto)); //TODO: additional validations? var eventTypeEntity = await internalUserTypeService.GetEventTypeEntityAsync(dto.EventType.ResourceId).ConfigureAwait(false) ?? throw new ResourceNotFoundException($"EventType with ResourceId {dto.EventType.ResourceId} not found."); @@ -34,7 +34,7 @@ public async Task AddUpdateEventAsync(EventDto dto) { } private Task AddUpdateEventPrivateAsync(Event @event, EventType eventType) { - ArgumentNullException.ThrowIfNull(eventType); + ArgumentNullException.ThrowIfNull(eventType, nameof(eventType)); // don't duplicate details - match on either Id or ResourceId @event.EventType = eventType; foreach (var detail in @event.Details) { @@ -49,7 +49,7 @@ private Task AddUpdateEventPrivateAsync(Event @event, EventType eventType return eventRepository.AddUpdateAsync(@event); } public async Task> AddUpdateEventsAsync(IEnumerable dtos) { - ArgumentNullException.ThrowIfNull(dtos); + ArgumentNullException.ThrowIfNull(dtos, nameof(dtos)); var events = new List(); foreach (var dto in dtos) { //TODO: additional validations? @@ -72,14 +72,14 @@ public async Task DeleteEventAsync(Guid resourceId) { } public async Task AddUpdateDetailAsync(Guid eventResourceId, DetailDto detailDto) { - ArgumentNullException.ThrowIfNull(detailDto); + ArgumentNullException.ThrowIfNull(detailDto, nameof(detailDto)); var result = AddUpdateDetailPrivateAsync(eventResourceId, mapper.Map(detailDto)); await eventRepository.SaveChangesAsync().ConfigureAwait(false); return mapper.Map(result); } private async Task AddUpdateDetailPrivateAsync(Guid eventResourceId, Detail detail) { - ArgumentNullException.ThrowIfNull(detail); + ArgumentNullException.ThrowIfNull(detail, nameof(detail)); var eventEntity = await eventRepository.GetByResourceIdAsync(eventResourceId).ConfigureAwait(false) ?? throw new ResourceNotFoundException($"Event with ResourceId {eventResourceId} not found."); var detailTypeEntity = await internalUserTypeService.GetDetailTypeEntityAsync(detail.DetailType.ResourceId).ConfigureAwait(false) ?? throw new ResourceNotFoundException($"DetailType with ResourceId {detail.DetailType.ResourceId} not found."); var intensityEntity = detailTypeEntity.AllowedIntensities.FirstOrDefault(i => i.ResourceId == detail.Intensity.ResourceId) ?? throw new ResourceNotFoundException($"Intensity with ResourceId {detail.Intensity.ResourceId} not found in DetailType with ResourceId {detail.DetailType.ResourceId}."); diff --git a/EventJournal.DomainService/UserTypesService.cs b/EventJournal.DomainService/UserTypesService.cs index f0cfd37..e90b8be 100644 --- a/EventJournal.DomainService/UserTypesService.cs +++ b/EventJournal.DomainService/UserTypesService.cs @@ -22,7 +22,7 @@ public async Task> GetAllEventTypesAsync() { } public async Task AddUpdateEventTypeAsync(EventTypeDto dto) { - ArgumentNullException.ThrowIfNull(dto); + ArgumentNullException.ThrowIfNull(dto, nameof(dto)); //TODO: additional validations? var savedEntity = await AddUpdateEventTypePrivateAsync(mapper.Map(dto)).ConfigureAwait(false); await eventTypeRepository.SaveChangesAsync().ConfigureAwait(false); @@ -32,7 +32,7 @@ private Task AddUpdateEventTypePrivateAsync(EventType entity) { return eventTypeRepository.AddUpdateAsync(entity); } public async Task> AddUpdateEventTypesAsync(IEnumerable dtos) { - ArgumentNullException.ThrowIfNull(dtos); + ArgumentNullException.ThrowIfNull(dtos, nameof(dtos)); List eventTypes = []; foreach (var dto in dtos) { EventType eventType = await AddUpdateEventTypePrivateAsync(mapper.Map(dto)).ConfigureAwait(false); @@ -64,7 +64,7 @@ public async Task> GetAllDetailTypesAsync() { } public async Task AddUpdateDetailTypeAsync(DetailTypeDto dto) { - ArgumentNullException.ThrowIfNull(dto); + ArgumentNullException.ThrowIfNull(dto, nameof(dto)); //TODO: additional validations? var entity = await AddUpdateDetailTypePrivateAsync(mapper.Map(dto)).ConfigureAwait(false); await detailTypeRepository.SaveChangesAsync().ConfigureAwait(false); @@ -83,7 +83,7 @@ private Task AddUpdateDetailTypePrivateAsync(DetailType entity) { return detailTypeRepository.AddUpdateAsync(entity); } public async Task> AddUpdateDetailTypesAsync(IEnumerable dtos) { - ArgumentNullException.ThrowIfNull(dtos); + ArgumentNullException.ThrowIfNull(dtos, nameof(dtos)); List detailTypeEntities = []; foreach (var dto in dtos) { detailTypeEntities.Add( await AddUpdateDetailTypePrivateAsync(mapper.Map(dto)).ConfigureAwait(false)); @@ -102,7 +102,7 @@ public async Task DeleteDetailTypeAsync(Guid resourceId) { } public async Task AddUpdateAllowedIntensityAsync(Guid detailTypeResourceId, IntensityDto intensityDto) { - ArgumentNullException.ThrowIfNull(intensityDto); + ArgumentNullException.ThrowIfNull(intensityDto, nameof(intensityDto)); var result = AddUpdateAllowedIntensityPrivateAsync(detailTypeResourceId, mapper.Map(intensityDto)); await detailTypeRepository.SaveChangesAsync().ConfigureAwait(false); return mapper.Map(result); diff --git a/EventJournal.Models/EventJournal.PublicModels.csproj b/EventJournal.Models/EventJournal.PublicModels.csproj index 2d94a24..478114e 100644 --- a/EventJournal.Models/EventJournal.PublicModels.csproj +++ b/EventJournal.Models/EventJournal.PublicModels.csproj @@ -6,6 +6,17 @@ enable + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/EventJournal.WebAPI/EventJournal.WebAPI.csproj b/EventJournal.WebAPI/EventJournal.WebAPI.csproj index 71d6689..8547395 100644 --- a/EventJournal.WebAPI/EventJournal.WebAPI.csproj +++ b/EventJournal.WebAPI/EventJournal.WebAPI.csproj @@ -14,18 +14,17 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/EventJournal.sln b/EventJournal.sln index 159982f..afa22b1 100644 --- a/EventJournal.sln +++ b/EventJournal.sln @@ -23,6 +23,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.PublicModels", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.Common", "EventJournal.Common\EventJournal.Common.csproj", "{1F36B474-30D4-4170-871A-76EEB7851439}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.BootStrap", "Eventjournal.Bootstrap\EventJournal.BootStrap.csproj", "{F07A04E5-0066-4E61-B783-24FC8E95D96C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -57,6 +59,10 @@ Global {1F36B474-30D4-4170-871A-76EEB7851439}.Debug|Any CPU.Build.0 = Debug|Any CPU {1F36B474-30D4-4170-871A-76EEB7851439}.Release|Any CPU.ActiveCfg = Release|Any CPU {1F36B474-30D4-4170-871A-76EEB7851439}.Release|Any CPU.Build.0 = Release|Any CPU + {F07A04E5-0066-4E61-B783-24FC8E95D96C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F07A04E5-0066-4E61-B783-24FC8E95D96C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F07A04E5-0066-4E61-B783-24FC8E95D96C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F07A04E5-0066-4E61-B783-24FC8E95D96C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Eventjournal.Bootstrap/DefaultApplicationBootStrapper.cs b/Eventjournal.Bootstrap/DefaultApplicationBootStrapper.cs new file mode 100644 index 0000000..d65f2cb --- /dev/null +++ b/Eventjournal.Bootstrap/DefaultApplicationBootStrapper.cs @@ -0,0 +1,14 @@ +using EventJournal.BootStrap.Installers; +using EventJournal.Common.Bootstrap; + +namespace EventJournal.BootStrap { + public class DefaultApplicationBootStrapper : BootStrapper { + public DefaultApplicationBootStrapper() { + installers = [ + new RepositoryInstaller(), + new DomainServiceInstaller(), + new DistributedLockInstaller(), + ]; + } + } +} diff --git a/Eventjournal.Bootstrap/EventJournal.BootStrap.csproj b/Eventjournal.Bootstrap/EventJournal.BootStrap.csproj new file mode 100644 index 0000000..96fc1ff --- /dev/null +++ b/Eventjournal.Bootstrap/EventJournal.BootStrap.csproj @@ -0,0 +1,26 @@ + + + + net9.0 + enable + enable + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/Eventjournal.Bootstrap/Installers/DistributedLockInstaller.cs b/Eventjournal.Bootstrap/Installers/DistributedLockInstaller.cs new file mode 100644 index 0000000..2efd1e8 --- /dev/null +++ b/Eventjournal.Bootstrap/Installers/DistributedLockInstaller.cs @@ -0,0 +1,16 @@ +using EventJournal.Common.Bootstrap; +using Medallion.Threading; +using Medallion.Threading.MySql; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace EventJournal.BootStrap.Installers { + public class DistributedLockInstaller : IInstaller { + public void Install(IServiceCollection services, IConfiguration configuration) { + var connectionString = configuration.GetSection("Database").GetValue("ConnectionString") + ?? throw new InvalidOperationException("Database:ConnectionString is not configured"); + IDistributedLockProvider provider = new MySqlDistributedSynchronizationProvider(connectionString); + services.AddSingleton(provider); + } + } +} diff --git a/Eventjournal.Bootstrap/Installers/DomainServiceInstaller.cs b/Eventjournal.Bootstrap/Installers/DomainServiceInstaller.cs new file mode 100644 index 0000000..a0aa7c0 --- /dev/null +++ b/Eventjournal.Bootstrap/Installers/DomainServiceInstaller.cs @@ -0,0 +1,13 @@ +using EventJournal.Common.Bootstrap; +using EventJournal.DomainService; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace EventJournal.BootStrap.Installers { + public class DomainServiceInstaller : IInstaller { + public void Install(IServiceCollection services, IConfiguration configuration) { + //TODO: does this work with multiple services? suspect it does + services.AddScopedInterfacesBySuffix("Service"); + } + } +} diff --git a/Eventjournal.Bootstrap/Installers/MiniProfilerInstaller.cs b/Eventjournal.Bootstrap/Installers/MiniProfilerInstaller.cs new file mode 100644 index 0000000..148660c --- /dev/null +++ b/Eventjournal.Bootstrap/Installers/MiniProfilerInstaller.cs @@ -0,0 +1,20 @@ +using EventJournal.Common.Bootstrap; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace EventJournal.BootStrap.Installers { + public class MiniProfilerInstaller : IInstaller { + public void Install(IServiceCollection services, IConfiguration configuration) { + services.AddMiniProfiler(options => { + options.RouteBasePath = "/profiler"; + + // Control which SQL formatter to use, InlineFormatter is the default + options.SqlFormatter = new StackExchange.Profiling.SqlFormatters.SqlServerFormatter(); + options.ColorScheme = StackExchange.Profiling.ColorScheme.Auto; + + // Enabled sending the Server-Timing header on responses + options.EnableServerTimingHeader = true; + }).AddEntityFramework(); + } + } +} diff --git a/Eventjournal.Bootstrap/Installers/ModelMapperInstaller.cs b/Eventjournal.Bootstrap/Installers/ModelMapperInstaller.cs new file mode 100644 index 0000000..e97f497 --- /dev/null +++ b/Eventjournal.Bootstrap/Installers/ModelMapperInstaller.cs @@ -0,0 +1,12 @@ +using EventJournal.Common.Bootstrap; +using EventJournal.DomainService; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace EventJournal.BootStrap.Installers { + public class ModelMapperInstaller :IInstaller { + public void Install(IServiceCollection services, IConfiguration configuration) { + services.AddAutoMapper(cfg => { }, typeof(DomainMapperProfile)); + } + } +} diff --git a/Eventjournal.Bootstrap/Installers/RepositoryInstaller.cs b/Eventjournal.Bootstrap/Installers/RepositoryInstaller.cs new file mode 100644 index 0000000..5a44e67 --- /dev/null +++ b/Eventjournal.Bootstrap/Installers/RepositoryInstaller.cs @@ -0,0 +1,13 @@ +using EventJournal.Common.Bootstrap; +using EventJournal.Data; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace EventJournal.BootStrap.Installers { + public class RepositoryInstaller : IInstaller { + public void Install(IServiceCollection services, IConfiguration configuration) { + //TODO: does this work with multiple repositories? suspect it does + services.AddScopedInterfacesBySuffix("Repository"); + } + } +} diff --git a/README.md b/README.md index 97de74b..585a484 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ The concept of Intensity is still a bit muddy, but the idea is that events of a - [Exercise Intensity could be](#exercise-intensity-could-be) - [Entity Framework help](#entity-framework-help) - [NEXT STEPS](#next-steps) - - [re not n](#re-not-n) - - [an id) and have journal e](#an-id-and-have-journal-e) + - [TODO](#todo) + - [Future Considerations](#future-considerations) ## Features @@ -63,9 +63,9 @@ EF https://learn.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs ### NEXT STEPS - add bootstrapper to setup - setup dependency injection - - default data seeding (mvoe default data provider to bootstrapper?) + - default data seeding (move default data provider to bootstrapper?) - move test data from defaultdataprovider to be inserted from UI - - (use this as an opportunity to add add methods to cli) + - (use this as an opportunity to add methods to cli) - still need to test adding event with details at the same time - unit tests for - repositories From ec023cd8d6e6cd08ef56c45a0ad196b5940aba12 Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Sun, 5 Oct 2025 17:45:44 -0400 Subject: [PATCH 31/34] Basic DI working, need to get apsettings/config working --- EventJournal.CLI/EventJournal.CLI.csproj | 6 +++ EventJournal.CLI/Program.cs | 49 ++++++++++--------- EventJournal.CLI/appsettings.json | 17 +++++++ .../Bootstrap/BootStrapperOptions.cs | 19 +++++++ .../Bootstrap/ServiceCollectionExtensions.cs | 21 +++++++- EventJournal.Common/IOC/DI.cs | 2 +- .../DefaultApplicationBootStrapper.cs | 1 + .../Installers/DomainServiceInstaller.cs | 1 - .../Installers/RepositoryInstaller.cs | 1 - README.md | 16 +++++- 10 files changed, 104 insertions(+), 29 deletions(-) create mode 100644 EventJournal.CLI/appsettings.json create mode 100644 EventJournal.Common/Bootstrap/BootStrapperOptions.cs diff --git a/EventJournal.CLI/EventJournal.CLI.csproj b/EventJournal.CLI/EventJournal.CLI.csproj index 90a5a71..7492c75 100644 --- a/EventJournal.CLI/EventJournal.CLI.csproj +++ b/EventJournal.CLI/EventJournal.CLI.csproj @@ -29,4 +29,10 @@ + + + + PreserveNewest + + diff --git a/EventJournal.CLI/Program.cs b/EventJournal.CLI/Program.cs index c80dc27..37297be 100644 --- a/EventJournal.CLI/Program.cs +++ b/EventJournal.CLI/Program.cs @@ -1,6 +1,7 @@ // See https://aka.ms/new-console-template for more information using EventJournal.BootStrap; using EventJournal.CLI; +using EventJournal.Common.Bootstrap; using EventJournal.Data; using EventJournal.Data.UserTypeRepositories; using EventJournal.DomainDto; @@ -80,33 +81,35 @@ private static async Task Main(string[] args) { } //TODO: move to shared bootsrapper.cs and remove this method and remove microsoft.extension.hosting pkg static IServiceProvider CreateServiceCollection() { + IConfiguration Configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .Build(); + var servicecollection = new ServiceCollection() - .AddDbContext(options => { - options.UseSqlite($"Data Source={DatabaseContext.GetSqliteDbPath()}"); - }) - .AddAutoMapper(cfg => { }, typeof(DomainMapperProfile)) - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddLogging(options => { - options.AddDebug(); - options.SetMinimumLevel(LogLevel.Error); - options.AddSimpleConsole(options => { - options.SingleLine = true; - options.TimestampFormat = "HH:mm:ss.fff "; - options.ColorBehavior = Microsoft.Extensions.Logging.Console.LoggerColorBehavior.Enabled; + //TOOD: move to bootstrapper and data source should be in appsettings.json + .AddDbContext(options => { + options.UseSqlite($"Data Source={DatabaseContext.GetSqliteDbPath()}"); + }) + //TODO: most/all of these should in bootstrapper (and may already be there) + .AddSingleton() + .AddLogging(options => { + options.AddDebug(); + options.SetMinimumLevel(LogLevel.Error); + options.AddSimpleConsole(options => { + options.SingleLine = true; + options.TimestampFormat = "HH:mm:ss.fff "; + options.ColorBehavior = Microsoft.Extensions.Logging.Console.LoggerColorBehavior.Enabled; + }); + }) + // setup and register boostrapper and it's installers -- needs to be last + .AddBootStrapper(Configuration, o => { + //TOOD: add any application specific installers here if needed + // installers that are common to all applications should go in the DefaultApplicationBootStrapper class }); - }); - // setup and register boostrapper and it's installers -- needs to be last - servicecollection.AddBootStrapper(Configuration, o => { - o.AddInstaller(new DomainServiceInstaller()); - o.AddInstaller(new IdentityServerInstaller()); - }); return servicecollection.BuildServiceProvider(); } + static Task AddTestDataAsync(IDefaultDataProvider defaultDataProvider) { Console.WriteLine("Adding/Resetting test data."); return defaultDataProvider.AddResetDefaultDataAsync(); diff --git a/EventJournal.CLI/appsettings.json b/EventJournal.CLI/appsettings.json new file mode 100644 index 0000000..3b4248f --- /dev/null +++ b/EventJournal.CLI/appsettings.json @@ -0,0 +1,17 @@ +{ + // Configuration settings for the EventJournal.CLI application + // TODO: clean up! + "AppSettings": { + "MySetting": "This is a setting value", + "AnotherSetting": 123 + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Database": { + "ConnectionString": "Server=myServer;Database=myDatabase;User ID=myUser;Password=myPassword;" + } +} \ No newline at end of file diff --git a/EventJournal.Common/Bootstrap/BootStrapperOptions.cs b/EventJournal.Common/Bootstrap/BootStrapperOptions.cs new file mode 100644 index 0000000..3dc7fc1 --- /dev/null +++ b/EventJournal.Common/Bootstrap/BootStrapperOptions.cs @@ -0,0 +1,19 @@ +using System.Collections.ObjectModel; + +namespace EventJournal.Common.Bootstrap { + public class BootStrapperOptions { + protected List installers; + + public BootStrapperOptions() { + installers = []; + } + + public void AddInstaller(IInstaller installer) { + installers.Add(installer); + } + + public ReadOnlyCollection Installers { + get { return installers.AsReadOnly(); } + } + } +} diff --git a/EventJournal.Common/Bootstrap/ServiceCollectionExtensions.cs b/EventJournal.Common/Bootstrap/ServiceCollectionExtensions.cs index 87d6854..0ea7059 100644 --- a/EventJournal.Common/Bootstrap/ServiceCollectionExtensions.cs +++ b/EventJournal.Common/Bootstrap/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using System.Reflection; namespace EventJournal.Common.Bootstrap { @@ -34,5 +35,23 @@ public static IServiceCollection AddSingletonClassesBySuffix(this IServiceCol return services; } + + public static IServiceCollection AddBootStrapper(this IServiceCollection services, + IConfiguration configuration, Action options) where T : BootStrapper, new() { + services.AddSingleton(configuration); + var bootstrapper = new T(); + + var o = new BootStrapperOptions(); + options?.Invoke(o); + + foreach (var installer in o.Installers) { + bootstrapper.AddInstaller(installer); + } + + //TODO: figure out if we need to cast to IConfigurationRoot + //bootstrapper.InitIoCContainer(configuration: (configuration as IConfigurationRoot), services); + bootstrapper.InitIoCContainer(configuration, services); + return services; + } } } diff --git a/EventJournal.Common/IOC/DI.cs b/EventJournal.Common/IOC/DI.cs index 71cf0b9..7e60a3b 100644 --- a/EventJournal.Common/IOC/DI.cs +++ b/EventJournal.Common/IOC/DI.cs @@ -2,7 +2,7 @@ namespace EventJournal.Common.IOC { public static class DI { - private static readonly object lockObject = new(); + private static readonly Lock lockObject = new(); public static void SetContainer(IServiceProvider instance) { lock (lockObject) { diff --git a/Eventjournal.Bootstrap/DefaultApplicationBootStrapper.cs b/Eventjournal.Bootstrap/DefaultApplicationBootStrapper.cs index d65f2cb..3fbbb51 100644 --- a/Eventjournal.Bootstrap/DefaultApplicationBootStrapper.cs +++ b/Eventjournal.Bootstrap/DefaultApplicationBootStrapper.cs @@ -8,6 +8,7 @@ public DefaultApplicationBootStrapper() { new RepositoryInstaller(), new DomainServiceInstaller(), new DistributedLockInstaller(), + new ModelMapperInstaller(), ]; } } diff --git a/Eventjournal.Bootstrap/Installers/DomainServiceInstaller.cs b/Eventjournal.Bootstrap/Installers/DomainServiceInstaller.cs index a0aa7c0..0533142 100644 --- a/Eventjournal.Bootstrap/Installers/DomainServiceInstaller.cs +++ b/Eventjournal.Bootstrap/Installers/DomainServiceInstaller.cs @@ -6,7 +6,6 @@ namespace EventJournal.BootStrap.Installers { public class DomainServiceInstaller : IInstaller { public void Install(IServiceCollection services, IConfiguration configuration) { - //TODO: does this work with multiple services? suspect it does services.AddScopedInterfacesBySuffix("Service"); } } diff --git a/Eventjournal.Bootstrap/Installers/RepositoryInstaller.cs b/Eventjournal.Bootstrap/Installers/RepositoryInstaller.cs index 5a44e67..fab123d 100644 --- a/Eventjournal.Bootstrap/Installers/RepositoryInstaller.cs +++ b/Eventjournal.Bootstrap/Installers/RepositoryInstaller.cs @@ -6,7 +6,6 @@ namespace EventJournal.BootStrap.Installers { public class RepositoryInstaller : IInstaller { public void Install(IServiceCollection services, IConfiguration configuration) { - //TODO: does this work with multiple repositories? suspect it does services.AddScopedInterfacesBySuffix("Repository"); } } diff --git a/README.md b/README.md index 585a484..742b5a3 100644 --- a/README.md +++ b/README.md @@ -62,8 +62,20 @@ EF https://learn.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs ### NEXT STEPS - add bootstrapper to setup - - setup dependency injection - - default data seeding (move default data provider to bootstrapper?) + - configuration for db context + - consider in memory and sqlite options + - consider add option for sql server (localdb or full) + - Add support for design time db context factory + - get configuration mapping working (appsettings.json) + - might need to add a config class for db settings + - might need to add a config class for default data settings + - might need to add a config class for logging settings + - might need to add a config class for web api settings (cors, etc) + - might need to add a config class for cli settings (verbosity, etc) + - might need to add nuget reference for Microsoft.Extensions.Configuration.Binder + - default data seeding + - add option to supply default data from config + - move default data provider to bootstrapper?) - move test data from defaultdataprovider to be inserted from UI - (use this as an opportunity to add methods to cli) - still need to test adding event with details at the same time From b1af4a4e7dc6b73e5024b62a3eb5b2850420ba09 Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Sun, 12 Oct 2025 19:00:45 -0400 Subject: [PATCH 32/34] Configuration settings work --- .../DesignTimeDbContextFactory.cs | 33 +++++++++++++++++ EventJournal.CLI/Program.cs | 30 +++------------- EventJournal.CLI/appsettings.json | 9 ++--- EventJournal.Common/Bootstrap/BootStrapper.cs | 2 +- .../Bootstrap/ServiceCollectionExtensions.cs | 14 ++++++-- .../DatabaseSettings.cs | 16 +++++++++ .../EventJournal.Configuration.csproj | 9 +++++ .../IServiceSettings.cs | 5 +++ EventJournal.Data/DatabaseContext.cs | 36 +++++++++++++------ EventJournal.Data/EventJournal.Data.csproj | 1 + EventJournal.DomainService/EventService.cs | 2 +- EventJournal.sln | 6 ++++ .../DefaultApplicationBootStrapper.cs | 8 +++-- .../EventJournal.BootStrap.csproj | 1 + .../Installers/DatabaseContextInstaller.cs | 20 +++++++++++ .../DefaultDataProviderInstaller.cs | 12 +++++++ .../Installers/DistributedLockInstaller.cs | 10 ++++-- .../Installers/LoggingInstaller.cs | 21 +++++++++++ README.md | 8 ++--- 19 files changed, 187 insertions(+), 56 deletions(-) create mode 100644 EventJournal.CLI/DesignTimeDbContextFactory.cs create mode 100644 EventJournal.Configuration/DatabaseSettings.cs create mode 100644 EventJournal.Configuration/EventJournal.Configuration.csproj create mode 100644 EventJournal.Configuration/IServiceSettings.cs create mode 100644 Eventjournal.Bootstrap/Installers/DatabaseContextInstaller.cs create mode 100644 Eventjournal.Bootstrap/Installers/DefaultDataProviderInstaller.cs create mode 100644 Eventjournal.Bootstrap/Installers/LoggingInstaller.cs diff --git a/EventJournal.CLI/DesignTimeDbContextFactory.cs b/EventJournal.CLI/DesignTimeDbContextFactory.cs new file mode 100644 index 0000000..086383f --- /dev/null +++ b/EventJournal.CLI/DesignTimeDbContextFactory.cs @@ -0,0 +1,33 @@ +using EventJournal.Configuration; +using EventJournal.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.Extensions.Configuration; + +namespace EventJournal.CLI { + public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory { + public DatabaseContext CreateDbContext(string[] args) { + IConfiguration configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json") + .Build(); + + var dbSettings = configuration.GetSection(DatabaseSettings.ConfigurationSectionName).Get() + ?? throw new InvalidOperationException("Configuration settings does not contain a valid DatabaseSettings section."); + + var builder = new DbContextOptionsBuilder(); + switch (dbSettings.DefaultProvider) { + default: + case DatabaseProvider.Sqlite: + builder.UseSqlite(DatabaseContext.GetConnectionString(dbSettings)); + break; + //case DatabaseProvider.SqlServer: + //break; + //case DatabaseProvider.PostgreSQL: + //break; + } + + return new DatabaseContext(builder.Options); + } + } +} diff --git a/EventJournal.CLI/Program.cs b/EventJournal.CLI/Program.cs index 37297be..c1451d3 100644 --- a/EventJournal.CLI/Program.cs +++ b/EventJournal.CLI/Program.cs @@ -2,15 +2,11 @@ using EventJournal.BootStrap; using EventJournal.CLI; using EventJournal.Common.Bootstrap; -using EventJournal.Data; -using EventJournal.Data.UserTypeRepositories; using EventJournal.DomainDto; using EventJournal.DomainService; using EventJournal.PublicModels; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using System.Text.Json; internal class Program { private static async Task Main(string[] args) { @@ -79,33 +75,17 @@ private static async Task Main(string[] args) { Console.WriteLine("\n=================================================\n"); } } - //TODO: move to shared bootsrapper.cs and remove this method and remove microsoft.extension.hosting pkg - static IServiceProvider CreateServiceCollection() { + + private static ServiceProvider CreateServiceCollection() { IConfiguration Configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .Build(); var servicecollection = new ServiceCollection() - //TOOD: move to bootstrapper and data source should be in appsettings.json - .AddDbContext(options => { - options.UseSqlite($"Data Source={DatabaseContext.GetSqliteDbPath()}"); - }) - //TODO: most/all of these should in bootstrapper (and may already be there) - .AddSingleton() - .AddLogging(options => { - options.AddDebug(); - options.SetMinimumLevel(LogLevel.Error); - options.AddSimpleConsole(options => { - options.SingleLine = true; - options.TimestampFormat = "HH:mm:ss.fff "; - options.ColorBehavior = Microsoft.Extensions.Logging.Console.LoggerColorBehavior.Enabled; - }); - }) - // setup and register boostrapper and it's installers -- needs to be last + // setup and register bootstrapper and it's installers -- needs to be last .AddBootStrapper(Configuration, o => { - //TOOD: add any application specific installers here if needed - // installers that are common to all applications should go in the DefaultApplicationBootStrapper class + // Add any application specific installers here. }); return servicecollection.BuildServiceProvider(); } @@ -128,7 +108,7 @@ static async Task DeleteAllDataAsync(IEventService eventService, IUserTypesServi } } - //TODO: move to shared utilities class or bootstrapper class and remove this method + // TODO: move to shared utilities class or bootstrapper class and remove this method private static JsonSerializerOptions GetSerializerOptions() { return DtoHelper.DefaultSerializerOptions; } diff --git a/EventJournal.CLI/appsettings.json b/EventJournal.CLI/appsettings.json index 3b4248f..263e7d2 100644 --- a/EventJournal.CLI/appsettings.json +++ b/EventJournal.CLI/appsettings.json @@ -1,10 +1,5 @@ { // Configuration settings for the EventJournal.CLI application - // TODO: clean up! - "AppSettings": { - "MySetting": "This is a setting value", - "AnotherSetting": 123 - }, "Logging": { "LogLevel": { "Default": "Information", @@ -12,6 +7,8 @@ } }, "Database": { - "ConnectionString": "Server=myServer;Database=myDatabase;User ID=myUser;Password=myPassword;" + //"ConnectionString": "Server=myServer;Database=myDatabase;User ID=myUser;Password=myPassword;", + "ConnectionString": "Default", + "DefaultProvider": "Sqlite" } } \ No newline at end of file diff --git a/EventJournal.Common/Bootstrap/BootStrapper.cs b/EventJournal.Common/Bootstrap/BootStrapper.cs index 00ca606..0cdcace 100644 --- a/EventJournal.Common/Bootstrap/BootStrapper.cs +++ b/EventJournal.Common/Bootstrap/BootStrapper.cs @@ -90,7 +90,7 @@ protected internal virtual IServiceProvider InternalInitialize(IConfiguration co DI.SetConfiguration(configuration); foreach (var i in installers) { - i.Install(services, configuration); + i.Install(services, configuration); } services.AddSingleton(configuration); diff --git a/EventJournal.Common/Bootstrap/ServiceCollectionExtensions.cs b/EventJournal.Common/Bootstrap/ServiceCollectionExtensions.cs index 0ea7059..ab459ce 100644 --- a/EventJournal.Common/Bootstrap/ServiceCollectionExtensions.cs +++ b/EventJournal.Common/Bootstrap/ServiceCollectionExtensions.cs @@ -36,9 +36,21 @@ public static IServiceCollection AddSingletonClassesBySuffix(this IServiceCol return services; } + // TODO: this is untested and may not work as expected + public static IServiceCollection AddSingletonClassesByInterface(this IServiceCollection services) where T : class { + typeof(T).GetTypeInfo().Assembly.GetTypes() + .Where(x => x.GetInterfaces().Contains(typeof(T)) + && x.GetTypeInfo().IsClass + && !x.GetTypeInfo().IsAbstract) + .ToList() + .ForEach(x => services.AddSingleton(x)); + + return services; + } public static IServiceCollection AddBootStrapper(this IServiceCollection services, IConfiguration configuration, Action options) where T : BootStrapper, new() { services.AddSingleton(configuration); + services.AddOptions(); var bootstrapper = new T(); var o = new BootStrapperOptions(); @@ -48,8 +60,6 @@ public static IServiceCollection AddBootStrapper(this IServiceCollection serv bootstrapper.AddInstaller(installer); } - //TODO: figure out if we need to cast to IConfigurationRoot - //bootstrapper.InitIoCContainer(configuration: (configuration as IConfigurationRoot), services); bootstrapper.InitIoCContainer(configuration, services); return services; } diff --git a/EventJournal.Configuration/DatabaseSettings.cs b/EventJournal.Configuration/DatabaseSettings.cs new file mode 100644 index 0000000..4e89157 --- /dev/null +++ b/EventJournal.Configuration/DatabaseSettings.cs @@ -0,0 +1,16 @@ +namespace EventJournal.Configuration { + public enum DatabaseProvider { + Sqlite, + // TODO: + // MySql, + // SqlServer, + // PostgreSQL + } + public class DatabaseSettings : IServiceSettings { + public static string ConfigurationSectionName => "Database"; + public string ConnectionString { get; set; } = null!; + public DatabaseProvider DefaultProvider { get; set; } = DatabaseProvider.Sqlite; + public bool UseDefaultConnectionString => string.IsNullOrWhiteSpace(ConnectionString) || ConnectionString.Equals("default", StringComparison.CurrentCultureIgnoreCase); + + } +} diff --git a/EventJournal.Configuration/EventJournal.Configuration.csproj b/EventJournal.Configuration/EventJournal.Configuration.csproj new file mode 100644 index 0000000..125f4c9 --- /dev/null +++ b/EventJournal.Configuration/EventJournal.Configuration.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/EventJournal.Configuration/IServiceSettings.cs b/EventJournal.Configuration/IServiceSettings.cs new file mode 100644 index 0000000..ad73581 --- /dev/null +++ b/EventJournal.Configuration/IServiceSettings.cs @@ -0,0 +1,5 @@ +namespace EventJournal.Configuration { + public interface IServiceSettings { + public static abstract string ConfigurationSectionName { get; } + } +} diff --git a/EventJournal.Data/DatabaseContext.cs b/EventJournal.Data/DatabaseContext.cs index c25a206..01dab86 100644 --- a/EventJournal.Data/DatabaseContext.cs +++ b/EventJournal.Data/DatabaseContext.cs @@ -1,7 +1,7 @@ -using EventJournal.Data.Entities; +using EventJournal.Configuration; +using EventJournal.Data.Entities; using EventJournal.Data.Entities.UserTypes; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace EventJournal.Data { @@ -18,7 +18,6 @@ public DatabaseContext() { public DatabaseContext(DbContextOptions options) : base(options) { // needed to properly inject DBContext at runtime - } //protected override void ConfigureConventions(ModelConfigurationBuilder builder) { @@ -36,16 +35,33 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { protected override void OnConfiguring(DbContextOptionsBuilder options) { if (!options.IsConfigured) { - options.UseSqlite($"Data Source={GetSqliteDbPath()}"); + options.UseSqlite(GetConnectionString()); } } - public static string GetSqliteDbPath() { - // The following configures EF to create a Sqlite database file in the - // special "local" folder for your platform. - var folder = Environment.SpecialFolder.LocalApplicationData; - var path = Environment.GetFolderPath(folder); - return Path.Join(path, "EventJournal.db"); + public static string GetConnectionString(DatabaseSettings? settings = null) { + + settings ??= new DatabaseSettings() { + ConnectionString = string.Empty, + DefaultProvider = DatabaseProvider.Sqlite + }; + + switch (settings.DefaultProvider) { + default: + case DatabaseProvider.Sqlite: + if (settings.UseDefaultConnectionString) { + // The following configures EF to create a Sqlite database file in the + // special "local" folder for your platform. + var path = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + return $"Data Source={Path.Join(path, "EventJournal.db")}"; + } + break; + //case DatabaseProvider.SqlServer: + //break; + //case DatabaseProvider.PostgreSQL: + //break; + } + return settings.ConnectionString; } public Task SaveChangesAsync() { diff --git a/EventJournal.Data/EventJournal.Data.csproj b/EventJournal.Data/EventJournal.Data.csproj index d14dc6e..c380937 100644 --- a/EventJournal.Data/EventJournal.Data.csproj +++ b/EventJournal.Data/EventJournal.Data.csproj @@ -24,6 +24,7 @@ + diff --git a/EventJournal.DomainService/EventService.cs b/EventJournal.DomainService/EventService.cs index 54f52a6..643a79d 100644 --- a/EventJournal.DomainService/EventService.cs +++ b/EventJournal.DomainService/EventService.cs @@ -8,7 +8,7 @@ namespace EventJournal.DomainService { public class EventService( IEventRepository eventRepository, - //TODO: fix this services shouldn't call services + //TODO: fix this. services shouldn't call other services IUserTypesService userTypesService, IMapper mapper) : IEventService { diff --git a/EventJournal.sln b/EventJournal.sln index afa22b1..cbd36d8 100644 --- a/EventJournal.sln +++ b/EventJournal.sln @@ -25,6 +25,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.Common", "Even EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.BootStrap", "Eventjournal.Bootstrap\EventJournal.BootStrap.csproj", "{F07A04E5-0066-4E61-B783-24FC8E95D96C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.Configuration", "EventJournal.Configuration\EventJournal.Configuration.csproj", "{B99D9E72-4ED2-4692-A073-987197FBB908}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -63,6 +65,10 @@ Global {F07A04E5-0066-4E61-B783-24FC8E95D96C}.Debug|Any CPU.Build.0 = Debug|Any CPU {F07A04E5-0066-4E61-B783-24FC8E95D96C}.Release|Any CPU.ActiveCfg = Release|Any CPU {F07A04E5-0066-4E61-B783-24FC8E95D96C}.Release|Any CPU.Build.0 = Release|Any CPU + {B99D9E72-4ED2-4692-A073-987197FBB908}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B99D9E72-4ED2-4692-A073-987197FBB908}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B99D9E72-4ED2-4692-A073-987197FBB908}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B99D9E72-4ED2-4692-A073-987197FBB908}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Eventjournal.Bootstrap/DefaultApplicationBootStrapper.cs b/Eventjournal.Bootstrap/DefaultApplicationBootStrapper.cs index 3fbbb51..a211b8c 100644 --- a/Eventjournal.Bootstrap/DefaultApplicationBootStrapper.cs +++ b/Eventjournal.Bootstrap/DefaultApplicationBootStrapper.cs @@ -5,10 +5,14 @@ namespace EventJournal.BootStrap { public class DefaultApplicationBootStrapper : BootStrapper { public DefaultApplicationBootStrapper() { installers = [ - new RepositoryInstaller(), - new DomainServiceInstaller(), + new DatabaseContextInstaller(), + new DefaultDataProviderInstaller(), new DistributedLockInstaller(), + new DomainServiceInstaller(), + new LoggingInstaller(), + new MiniProfilerInstaller(), new ModelMapperInstaller(), + new RepositoryInstaller(), ]; } } diff --git a/Eventjournal.Bootstrap/EventJournal.BootStrap.csproj b/Eventjournal.Bootstrap/EventJournal.BootStrap.csproj index 96fc1ff..b019a73 100644 --- a/Eventjournal.Bootstrap/EventJournal.BootStrap.csproj +++ b/Eventjournal.Bootstrap/EventJournal.BootStrap.csproj @@ -19,6 +19,7 @@ + diff --git a/Eventjournal.Bootstrap/Installers/DatabaseContextInstaller.cs b/Eventjournal.Bootstrap/Installers/DatabaseContextInstaller.cs new file mode 100644 index 0000000..5d34215 --- /dev/null +++ b/Eventjournal.Bootstrap/Installers/DatabaseContextInstaller.cs @@ -0,0 +1,20 @@ +using EventJournal.Common.Bootstrap; +using EventJournal.Configuration; +using EventJournal.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace EventJournal.BootStrap.Installers { + public class DatabaseContextInstaller : IInstaller { + public void Install(IServiceCollection services, IConfiguration configuration) { + + var dbSettings = configuration.GetSection(DatabaseSettings.ConfigurationSectionName).Get() + ?? throw new InvalidOperationException("Configuration settings does not contain a valid DatabaseSettings section."); + services.AddSingleton(dbSettings); + services.AddDbContext(options => { + options.UseSqlite(DatabaseContext.GetConnectionString(dbSettings)); + }); + } + } +} diff --git a/Eventjournal.Bootstrap/Installers/DefaultDataProviderInstaller.cs b/Eventjournal.Bootstrap/Installers/DefaultDataProviderInstaller.cs new file mode 100644 index 0000000..5ef3742 --- /dev/null +++ b/Eventjournal.Bootstrap/Installers/DefaultDataProviderInstaller.cs @@ -0,0 +1,12 @@ +using EventJournal.Common.Bootstrap; +using EventJournal.DomainService; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace EventJournal.BootStrap.Installers { + public class DefaultDataProviderInstaller : IInstaller { + public void Install(IServiceCollection services, IConfiguration configuration) { + services.AddScoped(); + } + } +} diff --git a/Eventjournal.Bootstrap/Installers/DistributedLockInstaller.cs b/Eventjournal.Bootstrap/Installers/DistributedLockInstaller.cs index 2efd1e8..cd1554b 100644 --- a/Eventjournal.Bootstrap/Installers/DistributedLockInstaller.cs +++ b/Eventjournal.Bootstrap/Installers/DistributedLockInstaller.cs @@ -1,4 +1,6 @@ using EventJournal.Common.Bootstrap; +using EventJournal.Configuration; +using EventJournal.Data; using Medallion.Threading; using Medallion.Threading.MySql; using Microsoft.Extensions.Configuration; @@ -7,9 +9,11 @@ namespace EventJournal.BootStrap.Installers { public class DistributedLockInstaller : IInstaller { public void Install(IServiceCollection services, IConfiguration configuration) { - var connectionString = configuration.GetSection("Database").GetValue("ConnectionString") - ?? throw new InvalidOperationException("Database:ConnectionString is not configured"); - IDistributedLockProvider provider = new MySqlDistributedSynchronizationProvider(connectionString); + var dbSettings = configuration.GetSection(DatabaseSettings.ConfigurationSectionName).Get() + ?? throw new InvalidOperationException("Configuration settings does not contain a valid DatabaseSettings section."); + + + IDistributedLockProvider provider = new MySqlDistributedSynchronizationProvider(DatabaseContext.GetConnectionString(dbSettings)); services.AddSingleton(provider); } } diff --git a/Eventjournal.Bootstrap/Installers/LoggingInstaller.cs b/Eventjournal.Bootstrap/Installers/LoggingInstaller.cs new file mode 100644 index 0000000..189d0fa --- /dev/null +++ b/Eventjournal.Bootstrap/Installers/LoggingInstaller.cs @@ -0,0 +1,21 @@ +using EventJournal.Common.Bootstrap; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace EventJournal.BootStrap.Installers { + public class LoggingInstaller : IInstaller { + public void Install(IServiceCollection services, IConfiguration configuration) { + // TODO: Make logging configuration driven eg. build options from config + services.AddLogging(options => { + options.AddDebug(); + options.SetMinimumLevel(LogLevel.Error); + options.AddSimpleConsole(options => { + options.SingleLine = true; + options.TimestampFormat = "HH:mm:ss.fff "; + options.ColorBehavior = Microsoft.Extensions.Logging.Console.LoggerColorBehavior.Enabled; + }); + }); + } + } +} diff --git a/README.md b/README.md index 742b5a3..d92232f 100644 --- a/README.md +++ b/README.md @@ -62,11 +62,7 @@ EF https://learn.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs ### NEXT STEPS - add bootstrapper to setup - - configuration for db context - - consider in memory and sqlite options - - consider add option for sql server (localdb or full) - - Add support for design time db context factory - - get configuration mapping working (appsettings.json) + - add config classes to handle settings and add config classes to bootstrap installers - might need to add a config class for db settings - might need to add a config class for default data settings - might need to add a config class for logging settings @@ -107,7 +103,7 @@ EF https://learn.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs - update event (with details) ### TODO -- Replace autoMapper with manual mapping +- Replace autoMapper with custom mappers - add swagger to web api project - Add common BootStrap code to be consumed by cli and web api projects - remove microsoft.extension.hosting pkg where not needed From 50936c31d891469d7bdc96b00350cd5d3c99ac34 Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Sun, 19 Oct 2025 19:06:19 -0400 Subject: [PATCH 33/34] Add JsonSeralizerSettings to boot strap, clean up how db settings was working and prepare for supporting other db providers, clean up DesignTimeDbContextFactory to remove duplicate config code --- .../DesignTimeDbContextFactory.cs | 13 +---- EventJournal.CLI/Program.cs | 19 +++---- EventJournal.CLI/appsettings.json | 2 +- .../Configuration}/DatabaseSettings.cs | 11 ++-- .../Configuration}/IServiceSettings.cs | 2 +- .../Configuration/JsonSerializerSettings.cs | 18 +++++++ EventJournal.Data/DatabaseContext.cs | 50 ++++++++++++++----- EventJournal.Data/EventJournal.Data.csproj | 1 - EventJournal.DomainModels/DtoHelper.cs | 15 ++---- EventJournal.sln | 6 --- .../DefaultApplicationBootStrapper.cs | 1 + .../EventJournal.BootStrap.csproj | 1 - .../Installers/DatabaseContextInstaller.cs | 12 ++--- .../Installers/DistributedLockInstaller.cs | 4 +- .../Installers/JsonOptionsInstaller.cs | 13 +++++ README.md | 46 ++++++++++------- 16 files changed, 124 insertions(+), 90 deletions(-) rename {EventJournal.Configuration => EventJournal.Common/Configuration}/DatabaseSettings.cs (67%) rename {EventJournal.Configuration => EventJournal.Common/Configuration}/IServiceSettings.cs (71%) create mode 100644 EventJournal.Common/Configuration/JsonSerializerSettings.cs create mode 100644 Eventjournal.Bootstrap/Installers/JsonOptionsInstaller.cs diff --git a/EventJournal.CLI/DesignTimeDbContextFactory.cs b/EventJournal.CLI/DesignTimeDbContextFactory.cs index 086383f..c8e585f 100644 --- a/EventJournal.CLI/DesignTimeDbContextFactory.cs +++ b/EventJournal.CLI/DesignTimeDbContextFactory.cs @@ -1,4 +1,4 @@ -using EventJournal.Configuration; +using EventJournal.Common.Configuration; using EventJournal.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; @@ -16,16 +16,7 @@ public DatabaseContext CreateDbContext(string[] args) { ?? throw new InvalidOperationException("Configuration settings does not contain a valid DatabaseSettings section."); var builder = new DbContextOptionsBuilder(); - switch (dbSettings.DefaultProvider) { - default: - case DatabaseProvider.Sqlite: - builder.UseSqlite(DatabaseContext.GetConnectionString(dbSettings)); - break; - //case DatabaseProvider.SqlServer: - //break; - //case DatabaseProvider.PostgreSQL: - //break; - } + DatabaseContext.ConfigureFromSettings(builder, dbSettings); return new DatabaseContext(builder.Options); } diff --git a/EventJournal.CLI/Program.cs b/EventJournal.CLI/Program.cs index c1451d3..d5f4cce 100644 --- a/EventJournal.CLI/Program.cs +++ b/EventJournal.CLI/Program.cs @@ -2,7 +2,6 @@ using EventJournal.BootStrap; using EventJournal.CLI; using EventJournal.Common.Bootstrap; -using EventJournal.DomainDto; using EventJournal.DomainService; using EventJournal.PublicModels; using Microsoft.Extensions.Configuration; @@ -15,6 +14,7 @@ private static async Task Main(string[] args) { var eventService = services.GetService() ?? throw new Exception("Unable to locate a valid Event Service"); var userTypeService = services.GetService() ?? throw new Exception("Unable to locate a valid User Types Service"); var defaultDataProvider = services.GetService() ?? throw new Exception("Unable to locate a valid Default Data Provider"); + var jsonSerializerOptions = services.GetService() ?? throw new Exception("Unable to locate a valid Json Serializer Options"); bool userIsDone = false; while (!userIsDone) { //Console.WriteLine("Type '1' to "); @@ -26,7 +26,7 @@ private static async Task Main(string[] args) { //Console.WriteLine("Type '8' to "); Console.WriteLine("Type 'v' to view all data"); - Console.WriteLine("Type 't' to add some test data."); + Console.WriteLine("Type 't' to add default test data."); Console.WriteLine("Type 'x' to delete all data."); Console.WriteLine("Type 'q' to quit."); @@ -63,7 +63,7 @@ private static async Task Main(string[] args) { //case '9': // break; case 'v': - await ViewallDataAsync(GetSerializerOptions(), eventService, userTypeService).ConfigureAwait(false); + await ViewallDataAsync(jsonSerializerOptions, eventService, userTypeService).ConfigureAwait(false); break; case 't': await AddTestDataAsync(defaultDataProvider).ConfigureAwait(false); @@ -81,13 +81,12 @@ private static ServiceProvider CreateServiceCollection() { .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .Build(); - - var servicecollection = new ServiceCollection() + + return new ServiceCollection() // setup and register bootstrapper and it's installers -- needs to be last .AddBootStrapper(Configuration, o => { // Add any application specific installers here. - }); - return servicecollection.BuildServiceProvider(); + }).BuildServiceProvider(); } static Task AddTestDataAsync(IDefaultDataProvider defaultDataProvider) { @@ -108,11 +107,6 @@ static async Task DeleteAllDataAsync(IEventService eventService, IUserTypesServi } } - // TODO: move to shared utilities class or bootstrapper class and remove this method - private static JsonSerializerOptions GetSerializerOptions() { - return DtoHelper.DefaultSerializerOptions; - } - static async Task ViewallDataAsync(JsonSerializerOptions options, IEventService eventService, IUserTypesService userTypeService) { EventDataResponseModel eventData = new() { DetailTypes = await userTypeService.GetAllDetailTypesAsync().ConfigureAwait(false), @@ -121,6 +115,5 @@ static async Task ViewallDataAsync(JsonSerializerOptions options, IEventService }; Console.WriteLine(JsonSerializer.Serialize(eventData, options)); - } } \ No newline at end of file diff --git a/EventJournal.CLI/appsettings.json b/EventJournal.CLI/appsettings.json index 263e7d2..40e4608 100644 --- a/EventJournal.CLI/appsettings.json +++ b/EventJournal.CLI/appsettings.json @@ -9,6 +9,6 @@ "Database": { //"ConnectionString": "Server=myServer;Database=myDatabase;User ID=myUser;Password=myPassword;", "ConnectionString": "Default", - "DefaultProvider": "Sqlite" + "Provider": "Sqlite" } } \ No newline at end of file diff --git a/EventJournal.Configuration/DatabaseSettings.cs b/EventJournal.Common/Configuration/DatabaseSettings.cs similarity index 67% rename from EventJournal.Configuration/DatabaseSettings.cs rename to EventJournal.Common/Configuration/DatabaseSettings.cs index 4e89157..c6f544d 100644 --- a/EventJournal.Configuration/DatabaseSettings.cs +++ b/EventJournal.Common/Configuration/DatabaseSettings.cs @@ -1,15 +1,14 @@ -namespace EventJournal.Configuration { +namespace EventJournal.Common.Configuration { public enum DatabaseProvider { Sqlite, - // TODO: - // MySql, - // SqlServer, - // PostgreSQL + MySql, + SqlServer, + PostgreSQL } public class DatabaseSettings : IServiceSettings { public static string ConfigurationSectionName => "Database"; public string ConnectionString { get; set; } = null!; - public DatabaseProvider DefaultProvider { get; set; } = DatabaseProvider.Sqlite; + public DatabaseProvider Provider { get; set; } = DatabaseProvider.Sqlite; public bool UseDefaultConnectionString => string.IsNullOrWhiteSpace(ConnectionString) || ConnectionString.Equals("default", StringComparison.CurrentCultureIgnoreCase); } diff --git a/EventJournal.Configuration/IServiceSettings.cs b/EventJournal.Common/Configuration/IServiceSettings.cs similarity index 71% rename from EventJournal.Configuration/IServiceSettings.cs rename to EventJournal.Common/Configuration/IServiceSettings.cs index ad73581..e301669 100644 --- a/EventJournal.Configuration/IServiceSettings.cs +++ b/EventJournal.Common/Configuration/IServiceSettings.cs @@ -1,4 +1,4 @@ -namespace EventJournal.Configuration { +namespace EventJournal.Common.Configuration { public interface IServiceSettings { public static abstract string ConfigurationSectionName { get; } } diff --git a/EventJournal.Common/Configuration/JsonSerializerSettings.cs b/EventJournal.Common/Configuration/JsonSerializerSettings.cs new file mode 100644 index 0000000..74056f8 --- /dev/null +++ b/EventJournal.Common/Configuration/JsonSerializerSettings.cs @@ -0,0 +1,18 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace EventJournal.Common.Configuration { + // this class is static so helper methods can consume it without dependency injection + // if you need a instantiated version, use JsonSerailizerOptions installed via bootstrap + public static class JsonSerializerSettings { + public static JsonSerializerOptions JsonSerializerOptions { get; set; } = new() { + WriteIndented = true, + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = { + new JsonStringEnumConverter() + // TODO: add additional converters as needed + } + }; + } +} diff --git a/EventJournal.Data/DatabaseContext.cs b/EventJournal.Data/DatabaseContext.cs index 01dab86..9362bed 100644 --- a/EventJournal.Data/DatabaseContext.cs +++ b/EventJournal.Data/DatabaseContext.cs @@ -1,4 +1,4 @@ -using EventJournal.Configuration; +using EventJournal.Common.Configuration; using EventJournal.Data.Entities; using EventJournal.Data.Entities.UserTypes; using Microsoft.EntityFrameworkCore; @@ -13,13 +13,14 @@ public class DatabaseContext : DbContext, IDatabaseContext { public DbSet Intensities { get; set; } = null!; public DatabaseContext() { - // needed by EF cli tools + // needed by EF cli tools + // TODO: is it needed if we have the DesignTimeDbContextFactory? } public DatabaseContext(DbContextOptions options) : base(options) { // needed to properly inject DBContext at runtime } - + // TODO: do we need enumeration conversion? //protected override void ConfigureConventions(ModelConfigurationBuilder builder) { // // Applies conversion to all enumerations // _ = builder.Properties() @@ -34,31 +35,56 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { } protected override void OnConfiguring(DbContextOptionsBuilder options) { + //TODO: this isn't working as expected, options isn't the one created in the installer if (!options.IsConfigured) { - options.UseSqlite(GetConnectionString()); + base.OnConfiguring(options); + // options.UseSqlite(GetConnectionString()); + } + } + + public static DbContextOptionsBuilder ConfigureFromSettings(DbContextOptionsBuilder options, DatabaseSettings? settings = null) { + settings ??= DefaultSettings; + switch (settings.Provider) { + default: + case DatabaseProvider.Sqlite: + options.UseSqlite(GetConnectionString(settings)); + break; + //case DatabaseProvider.MySql: + // optionsBuilder.UseMySql(GetConnectionString(settings), ServerVersion.AutoDetect(GetConnectionString(settings))); + // break; + //case DatabaseProvider.SqlServer: + // optionsBuilder.UseSqlServer(GetConnectionString(settings)); + // break; + //case DatabaseProvider.PostgreSQL: + // optionsBuilder.UseNpgsql(GetConnectionString(settings)); + // break; } + return options; } + private static DatabaseSettings DefaultSettings { get; set; } = new DatabaseSettings() { + ConnectionString = string.Empty, + Provider = DatabaseProvider.Sqlite + }; + // TODO: make private or internal after we get DI for distributed lock supporting multiple DB providers public static string GetConnectionString(DatabaseSettings? settings = null) { - settings ??= new DatabaseSettings() { - ConnectionString = string.Empty, - DefaultProvider = DatabaseProvider.Sqlite - }; + settings ??= DefaultSettings; - switch (settings.DefaultProvider) { + switch (settings.Provider) { default: case DatabaseProvider.Sqlite: if (settings.UseDefaultConnectionString) { // The following configures EF to create a Sqlite database file in the // special "local" folder for your platform. var path = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - return $"Data Source={Path.Join(path, "EventJournal.db")}"; + var program = System.Reflection.Assembly.GetEntryAssembly()?.GetName().Name!.Split(".")[0]; + return $"Data Source={Path.Join(path, $"{program}.db")}"; } break; - //case DatabaseProvider.SqlServer: + //case DatabaseProvider.SqlServer: //break; - //case DatabaseProvider.PostgreSQL: + //case DatabaseProvider.PostgreSQL: //break; } return settings.ConnectionString; diff --git a/EventJournal.Data/EventJournal.Data.csproj b/EventJournal.Data/EventJournal.Data.csproj index c380937..d14dc6e 100644 --- a/EventJournal.Data/EventJournal.Data.csproj +++ b/EventJournal.Data/EventJournal.Data.csproj @@ -24,7 +24,6 @@ - diff --git a/EventJournal.DomainModels/DtoHelper.cs b/EventJournal.DomainModels/DtoHelper.cs index aca7310..c296c2c 100644 --- a/EventJournal.DomainModels/DtoHelper.cs +++ b/EventJournal.DomainModels/DtoHelper.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using EventJournal.Common.Configuration; +using System.Text.Json; namespace EventJournal.DomainDto { public static partial class DtoHelper { @@ -17,21 +17,14 @@ public static T UpdateDTO(this T destination, T source) where T : BaseDto { } public static string Serialize(this T entity, JsonSerializerOptions? serializerOptions = null) where T : BaseDto { - return JsonSerializer.Serialize(entity, entity.GetType(), serializerOptions ?? DefaultSerializerOptions); + return JsonSerializer.Serialize(entity, entity.GetType(), serializerOptions ?? JsonSerializerSettings.JsonSerializerOptions); } public static string Serialize(this IEnumerable list, JsonSerializerOptions? serializerOptions = null) where T : BaseDto { - return JsonSerializer.Serialize(list, list.GetType(), serializerOptions ?? DefaultSerializerOptions); + return JsonSerializer.Serialize(list, list.GetType(), serializerOptions ?? JsonSerializerSettings.JsonSerializerOptions); } //TODO: options should be setup in bootstrap - public static readonly JsonSerializerOptions DefaultSerializerOptions = new() { - WriteIndented = true, - PropertyNameCaseInsensitive = true, - //TODO: do we want this? - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - Converters = { new JsonStringEnumConverter() } - }; public static T? Deserialize(this string json) where T : BaseDto { //TODO: does this work properly for inherited types? diff --git a/EventJournal.sln b/EventJournal.sln index cbd36d8..afa22b1 100644 --- a/EventJournal.sln +++ b/EventJournal.sln @@ -25,8 +25,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.Common", "Even EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.BootStrap", "Eventjournal.Bootstrap\EventJournal.BootStrap.csproj", "{F07A04E5-0066-4E61-B783-24FC8E95D96C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventJournal.Configuration", "EventJournal.Configuration\EventJournal.Configuration.csproj", "{B99D9E72-4ED2-4692-A073-987197FBB908}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -65,10 +63,6 @@ Global {F07A04E5-0066-4E61-B783-24FC8E95D96C}.Debug|Any CPU.Build.0 = Debug|Any CPU {F07A04E5-0066-4E61-B783-24FC8E95D96C}.Release|Any CPU.ActiveCfg = Release|Any CPU {F07A04E5-0066-4E61-B783-24FC8E95D96C}.Release|Any CPU.Build.0 = Release|Any CPU - {B99D9E72-4ED2-4692-A073-987197FBB908}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B99D9E72-4ED2-4692-A073-987197FBB908}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B99D9E72-4ED2-4692-A073-987197FBB908}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B99D9E72-4ED2-4692-A073-987197FBB908}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Eventjournal.Bootstrap/DefaultApplicationBootStrapper.cs b/Eventjournal.Bootstrap/DefaultApplicationBootStrapper.cs index a211b8c..95532b9 100644 --- a/Eventjournal.Bootstrap/DefaultApplicationBootStrapper.cs +++ b/Eventjournal.Bootstrap/DefaultApplicationBootStrapper.cs @@ -9,6 +9,7 @@ public DefaultApplicationBootStrapper() { new DefaultDataProviderInstaller(), new DistributedLockInstaller(), new DomainServiceInstaller(), + new JsonOptionsInstaller(), new LoggingInstaller(), new MiniProfilerInstaller(), new ModelMapperInstaller(), diff --git a/Eventjournal.Bootstrap/EventJournal.BootStrap.csproj b/Eventjournal.Bootstrap/EventJournal.BootStrap.csproj index b019a73..96fc1ff 100644 --- a/Eventjournal.Bootstrap/EventJournal.BootStrap.csproj +++ b/Eventjournal.Bootstrap/EventJournal.BootStrap.csproj @@ -19,7 +19,6 @@ - diff --git a/Eventjournal.Bootstrap/Installers/DatabaseContextInstaller.cs b/Eventjournal.Bootstrap/Installers/DatabaseContextInstaller.cs index 5d34215..7231695 100644 --- a/Eventjournal.Bootstrap/Installers/DatabaseContextInstaller.cs +++ b/Eventjournal.Bootstrap/Installers/DatabaseContextInstaller.cs @@ -1,7 +1,6 @@ using EventJournal.Common.Bootstrap; -using EventJournal.Configuration; +using EventJournal.Common.Configuration; using EventJournal.Data; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -9,12 +8,11 @@ namespace EventJournal.BootStrap.Installers { public class DatabaseContextInstaller : IInstaller { public void Install(IServiceCollection services, IConfiguration configuration) { - var dbSettings = configuration.GetSection(DatabaseSettings.ConfigurationSectionName).Get() + var dbSettings = configuration.GetSection(DatabaseSettings.ConfigurationSectionName).Get() ?? throw new InvalidOperationException("Configuration settings does not contain a valid DatabaseSettings section."); - services.AddSingleton(dbSettings); - services.AddDbContext(options => { - options.UseSqlite(DatabaseContext.GetConnectionString(dbSettings)); - }); + services.AddSingleton(dbSettings); + + services.AddDbContext(o => DatabaseContext.ConfigureFromSettings(o, dbSettings)); } } } diff --git a/Eventjournal.Bootstrap/Installers/DistributedLockInstaller.cs b/Eventjournal.Bootstrap/Installers/DistributedLockInstaller.cs index cd1554b..c77501d 100644 --- a/Eventjournal.Bootstrap/Installers/DistributedLockInstaller.cs +++ b/Eventjournal.Bootstrap/Installers/DistributedLockInstaller.cs @@ -1,5 +1,5 @@ using EventJournal.Common.Bootstrap; -using EventJournal.Configuration; +using EventJournal.Common.Configuration; using EventJournal.Data; using Medallion.Threading; using Medallion.Threading.MySql; @@ -12,7 +12,7 @@ public void Install(IServiceCollection services, IConfiguration configuration) { var dbSettings = configuration.GetSection(DatabaseSettings.ConfigurationSectionName).Get() ?? throw new InvalidOperationException("Configuration settings does not contain a valid DatabaseSettings section."); - + // TODO: configure based on Database PRovider from settings IDistributedLockProvider provider = new MySqlDistributedSynchronizationProvider(DatabaseContext.GetConnectionString(dbSettings)); services.AddSingleton(provider); } diff --git a/Eventjournal.Bootstrap/Installers/JsonOptionsInstaller.cs b/Eventjournal.Bootstrap/Installers/JsonOptionsInstaller.cs new file mode 100644 index 0000000..b91c1aa --- /dev/null +++ b/Eventjournal.Bootstrap/Installers/JsonOptionsInstaller.cs @@ -0,0 +1,13 @@ +using EventJournal.Common.Bootstrap; +using EventJournal.Common.Configuration; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace EventJournal.BootStrap.Installers { + public class JsonOptionsInstaller : IInstaller { + public void Install(IServiceCollection services, IConfiguration configuration) { + // make changes to JsonSerializerOptions in the static JsonSerializerSettings if needed + services.AddSingleton(JsonSerializerSettings.JsonSerializerOptions); + } + } +} diff --git a/README.md b/README.md index d92232f..db8a6a2 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,9 @@ The concept of Intensity is still a bit muddy, but the idea is that events of a - [Bleeding could be](#bleeding-could-be) - [Exercise Intensity could be](#exercise-intensity-could-be) - [Entity Framework help](#entity-framework-help) - - [NEXT STEPS](#next-steps) - [TODO](#todo) + - [NEXT STEPS](#next-steps) + - [Future TODOs](#future-todos) - [Future Considerations](#future-considerations) ## Features @@ -60,20 +61,31 @@ Events are the central entity. An `Event` has an `EventType` and one or more `De EF https://learn.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs=netcore-cli -### NEXT STEPS -- add bootstrapper to setup - - add config classes to handle settings and add config classes to bootstrap installers - - might need to add a config class for db settings - - might need to add a config class for default data settings - - might need to add a config class for logging settings - - might need to add a config class for web api settings (cors, etc) - - might need to add a config class for cli settings (verbosity, etc) - - might need to add nuget reference for Microsoft.Extensions.Configuration.Binder - - default data seeding - - add option to supply default data from config - - move default data provider to bootstrapper?) - - move test data from defaultdataprovider to be inserted from UI - - (use this as an opportunity to add methods to cli) + + +### TODO +#### NEXT STEPS +- add config classes to handle settings including adding the new classes to bootstrap installers + - extend dbsettings to support other db providers (sqlite, postgresql, mysql, etc) + - see if TODO item about supporting other db providers in dbcontext static methods can be done here + - see if TODO item in databasecontext about the empty constructor being needed + - TEST EF Migrations with new db context configuration code + - this means adding nuget references for the other db providers + - as well as extending the db settings class to have provider type + - and rounding out the static methods in dbcontext itself to handle other providers + - TEST EF migrations with other db providers + - might need to add a config class for default data settings + - might need to add a config class for logging settings + - might need to add a config class for web api settings (cors, etc) + - might need to add a config class for cli settings (verbosity, etc) + - might need to add nuget reference for Microsoft.Extensions.Configuration.Binder +- scan code for TODO comments and address them or add them to future todo list +- default data seeding + - ?add option to supply default data from config? +- add ability to add data in the cli project + - add commands to add event types + - add commands to add detail types (with allowed intensities) + - add commands to add events (with details) - still need to test adding event with details at the same time - unit tests for - repositories @@ -85,7 +97,7 @@ EF https://learn.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs - bootstrapper - other helpers/extensions? - ?? web api controllers - +#### Future TODOs - test full get event with details - test full get detail type with allowed intensities - add endpoints for: @@ -101,8 +113,6 @@ EF https://learn.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs - update event type - update detail type (with allowed intensities) - update event (with details) - -### TODO - Replace autoMapper with custom mappers - add swagger to web api project - Add common BootStrap code to be consumed by cli and web api projects From a1630c79aa14889317b323462da588108a97ebc1 Mon Sep 17 00:00:00 2001 From: "John W. Stokes, Jr." Date: Sun, 30 Nov 2025 15:56:15 -0500 Subject: [PATCH 34/34] version updates --- EventJournal.CLI/EventJournal.CLI.csproj | 3 ++- EventJournal.Common/EventJournal.Common.csproj | 4 +--- EventJournal.Data/EventJournal.Data.csproj | 4 ++-- EventJournal.DomainModels/EventJournal.DomainDto.csproj | 1 + .../EventJournal.DomainService.csproj | 2 +- EventJournal.Models/EventJournal.PublicModels.csproj | 1 + EventJournal.WebAPI/EventJournal.WebAPI.csproj | 4 ++-- README.md | 7 ++++++- 8 files changed, 16 insertions(+), 10 deletions(-) diff --git a/EventJournal.CLI/EventJournal.CLI.csproj b/EventJournal.CLI/EventJournal.CLI.csproj index 7492c75..1f42fea 100644 --- a/EventJournal.CLI/EventJournal.CLI.csproj +++ b/EventJournal.CLI/EventJournal.CLI.csproj @@ -13,10 +13,11 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/EventJournal.Common/EventJournal.Common.csproj b/EventJournal.Common/EventJournal.Common.csproj index a8d35e3..325572b 100644 --- a/EventJournal.Common/EventJournal.Common.csproj +++ b/EventJournal.Common/EventJournal.Common.csproj @@ -13,9 +13,7 @@ - - - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/EventJournal.Data/EventJournal.Data.csproj b/EventJournal.Data/EventJournal.Data.csproj index d14dc6e..08bb9d3 100644 --- a/EventJournal.Data/EventJournal.Data.csproj +++ b/EventJournal.Data/EventJournal.Data.csproj @@ -11,11 +11,11 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/EventJournal.DomainModels/EventJournal.DomainDto.csproj b/EventJournal.DomainModels/EventJournal.DomainDto.csproj index a12a7a8..6429f43 100644 --- a/EventJournal.DomainModels/EventJournal.DomainDto.csproj +++ b/EventJournal.DomainModels/EventJournal.DomainDto.csproj @@ -11,6 +11,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/EventJournal.DomainService/EventJournal.DomainService.csproj b/EventJournal.DomainService/EventJournal.DomainService.csproj index d1f7aed..0bc6208 100644 --- a/EventJournal.DomainService/EventJournal.DomainService.csproj +++ b/EventJournal.DomainService/EventJournal.DomainService.csproj @@ -11,7 +11,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/EventJournal.Models/EventJournal.PublicModels.csproj b/EventJournal.Models/EventJournal.PublicModels.csproj index 478114e..27241db 100644 --- a/EventJournal.Models/EventJournal.PublicModels.csproj +++ b/EventJournal.Models/EventJournal.PublicModels.csproj @@ -11,6 +11,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/EventJournal.WebAPI/EventJournal.WebAPI.csproj b/EventJournal.WebAPI/EventJournal.WebAPI.csproj index 8547395..27c6517 100644 --- a/EventJournal.WebAPI/EventJournal.WebAPI.csproj +++ b/EventJournal.WebAPI/EventJournal.WebAPI.csproj @@ -14,11 +14,11 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all diff --git a/README.md b/README.md index db8a6a2..4125f0d 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,12 @@ EF https://learn.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs ### TODO #### NEXT STEPS -- add config classes to handle settings including adding the new classes to bootstrap installers +- update dotnet EF to latest version - seems to be somethign wrong with runnign dotnet tool update -g + - might have version conflicts with version 9 vs version 10 +- Consider updatating to .net 10 +- finnish add config classes to handle settings including adding the new classes to bootstrap installers + - not sure if something is wrong with DB config but comments in the code suggest there might be + - need to refamiliarize self with how configuration is configured here - extend dbsettings to support other db providers (sqlite, postgresql, mysql, etc) - see if TODO item about supporting other db providers in dbcontext static methods can be done here - see if TODO item in databasecontext about the empty constructor being needed