From 9dda0b9f9407ea5abb6949c0ed8b8d68aceb3e1b Mon Sep 17 00:00:00 2001 From: Dave Thaler Date: Tue, 11 Jun 2024 14:48:22 -0700 Subject: [PATCH 1/3] Add IFTTT APIs Signed-off-by: Dave Thaler --- .../Api/NodeStateEventsController.cs | 34 ++-- OrcanodeMonitor/Api/OrcanodesController.cs | 184 ++++++++++++++++++ OrcanodeMonitor/Api/StatusController.cs | 25 +++ OrcanodeMonitor/Api/TestSetupController.cs | 39 ++++ OrcanodeMonitor/Core/Fetcher.cs | 28 +++ OrcanodeMonitor/Models/Orcanode.cs | 19 ++ OrcanodeMonitor/Models/OrcanodeEvent.cs | 2 +- docs/Design.md | 2 +- 8 files changed, 320 insertions(+), 13 deletions(-) create mode 100644 OrcanodeMonitor/Api/OrcanodesController.cs create mode 100644 OrcanodeMonitor/Api/StatusController.cs create mode 100644 OrcanodeMonitor/Api/TestSetupController.cs diff --git a/OrcanodeMonitor/Api/NodeStateEventsController.cs b/OrcanodeMonitor/Api/NodeStateEventsController.cs index d11f09c..f147aec 100644 --- a/OrcanodeMonitor/Api/NodeStateEventsController.cs +++ b/OrcanodeMonitor/Api/NodeStateEventsController.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using OrcanodeMonitor.Core; using OrcanodeMonitor.Data; using OrcanodeMonitor.Models; using System.Dynamic; @@ -12,6 +13,7 @@ namespace OrcanodeMonitor.Api [ApiController] public class NodeStateEventsController : ControllerBase { + const int _defaultLimit = 50; private readonly OrcanodeMonitorContext _databaseContext; public NodeStateEventsController(OrcanodeMonitorContext context) @@ -41,28 +43,38 @@ private JsonResult GetEvents(int limit) return new JsonResult(jsonElement); } + // GET is not used by IFTTT so this does not require a service key. // GET: api/ifttt/v1/triggers/ [HttpGet] - public JsonResult Get() + public IActionResult Get() { - return GetEvents(50); + return GetEvents(_defaultLimit); } // POST api/ifttt/v1/triggers/ [HttpPost] - public IActionResult Post([FromBody] string value) + public IActionResult Post([FromBody] JsonElement requestBody) { + ObjectResult failure = Fetcher.CheckIftttServiceKey(Request); + if (failure != null) + { + return failure; + } + + if (requestBody.ValueKind != JsonValueKind.Object) + { + return BadRequest("Invalid JSON data."); + } + try { - dynamic requestBody = JsonSerializer.Deserialize(value); - if (!requestBody.TryGetProperty("limit", out JsonElement limitElement)) - { - return BadRequest("Invalid JSON data."); - } - int limit = 50; - if (limitElement.TryGetInt32(out int explicitLimit)) + int limit = _defaultLimit; + if (requestBody.TryGetProperty("limit", out JsonElement limitElement)) { - limit = explicitLimit; + if (limitElement.TryGetInt32(out int explicitLimit)) + { + limit = explicitLimit; + } } if (requestBody.TryGetProperty("triggerFields", out JsonElement triggerFields)) { diff --git a/OrcanodeMonitor/Api/OrcanodesController.cs b/OrcanodeMonitor/Api/OrcanodesController.cs new file mode 100644 index 0000000..ec8764b --- /dev/null +++ b/OrcanodeMonitor/Api/OrcanodesController.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using OrcanodeMonitor.Core; +using OrcanodeMonitor.Data; +using OrcanodeMonitor.Models; + +namespace OrcanodeMonitor.Api +{ + [Route("api/ifttt/v1/queries/[controller]")] + [ApiController] + public class OrcanodesController : ControllerBase + { + const int _defaultLimit = 50; + private readonly OrcanodeMonitorContext _databaseContext; + + public OrcanodesController(OrcanodeMonitorContext context) + { + _databaseContext = context; + } + + // GET is not used by IFTTT so this does not require a service key. + // GET: api/ifttt/v1/queries/Orcanodes + [HttpGet] + public async Task>> GetOrcanode() + { + return await GetJsonNodesAsync(_defaultLimit); + } + +#if false + // GET: api/ifttt/v1/queries/orcanodes/5 + [HttpGet("{id}")] + public async Task> GetOrcanode(int id) + { + var orcanode = await _databaseContext.Orcanodes.FindAsync(id); + + if (orcanode == null) + { + return NotFound(); + } + + return orcanode; + } +#endif + + private async Task GetJsonNodesAsync(int limit) + { + var nodes = await _databaseContext.Orcanodes.ToListAsync(); + + // Convert to IFTTT data transfer objects. + var jsonNodes = new List(); + foreach (Orcanode node in nodes) + { + jsonNodes.Add(node.ToIftttDTO()); + } + + var dataResult = new { data = jsonNodes }; + + var jsonString = JsonSerializer.Serialize(dataResult); + var jsonDocument = JsonDocument.Parse(jsonString); + + // Get the JSON data as an array. + var jsonElement = jsonDocument.RootElement; + + return new JsonResult(jsonElement); + } + + // POST: api/ifttt/v1/queries/orcanodes + // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754 + [HttpPost] + public async Task> PostOrcanode([FromBody] JsonElement requestBody) + { + ObjectResult failure = Fetcher.CheckIftttServiceKey(Request); + if (failure != null) + { + return failure; + } + + try + { + int limit = _defaultLimit; + if (requestBody.TryGetProperty("limit", out JsonElement limitElement)) + { + if (limitElement.TryGetInt32(out int explicitLimit)) + { + limit = explicitLimit; + } + } + if (requestBody.TryGetProperty("queryFields", out JsonElement triggerFields)) + { + // TODO: use queryFields. + } + + return await GetJsonNodesAsync(limit); + } + catch (JsonException) + { + return BadRequest("Invalid JSON data."); + } + } + +#if false + // POST: api/ifttt/v1/queries/orcanodes + // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754 + [HttpPost] + public async Task> PostOrcanode(Orcanode orcanode) + { + _databaseContext.Orcanodes.Add(orcanode); + await _databaseContext.SaveChangesAsync(); + + return CreatedAtAction("GetOrcanode", new { id = orcanode.ID }, orcanode); + } + + // PUT: api/ifttt/v1/queries/orcanodes/5 + // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754 + [HttpPut("{id}")] + public async Task PutOrcanode(int id, Orcanode orcanode) + { + if (id != orcanode.ID) + { + return BadRequest(); + } + + _databaseContext.Entry(orcanode).State = EntityState.Modified; + + try + { + await _databaseContext.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + if (!OrcanodeExists(id)) + { + return NotFound(); + } + else + { + throw; + } + } + + return NoContent(); + } + + // POST: api/ifttt/v1/queries/orcanodes + // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754 + [HttpPost] + public async Task> PostOrcanode(Orcanode orcanode) + { + _databaseContext.Orcanodes.Add(orcanode); + await _databaseContext.SaveChangesAsync(); + + return CreatedAtAction("GetOrcanode", new { id = orcanode.ID }, orcanode); + } + + // DELETE: api/ifttt/v1/queries/orcanodes/5 + [HttpDelete("{id}")] + public async Task DeleteOrcanode(int id) + { + var orcanode = await _databaseContext.Orcanodes.FindAsync(id); + if (orcanode == null) + { + return NotFound(); + } + + _databaseContext.Orcanodes.Remove(orcanode); + await _databaseContext.SaveChangesAsync(); + + return NoContent(); + } + + private bool OrcanodeExists(int id) + { + return _databaseContext.Orcanodes.Any(e => e.ID == id); + } +#endif + } +} diff --git a/OrcanodeMonitor/Api/StatusController.cs b/OrcanodeMonitor/Api/StatusController.cs new file mode 100644 index 0000000..ffac09d --- /dev/null +++ b/OrcanodeMonitor/Api/StatusController.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Mvc; +using OrcanodeMonitor.Core; +using System.Net; + +// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 + +namespace OrcanodeMonitor.Api +{ + [Route("api/ifttt/v1/[controller]")] + [ApiController] + public class StatusController : ControllerBase + { + // GET: api/ifttt/v1/ + [HttpGet] + public IActionResult Get() + { + ObjectResult failure = Fetcher.CheckIftttServiceKey(Request); + if (failure != null) + { + return failure; + } + return Ok(); + } + } +} diff --git a/OrcanodeMonitor/Api/TestSetupController.cs b/OrcanodeMonitor/Api/TestSetupController.cs new file mode 100644 index 0000000..fc467a7 --- /dev/null +++ b/OrcanodeMonitor/Api/TestSetupController.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Mvc; +using OrcanodeMonitor.Core; +using System.Net; + +// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 + +namespace OrcanodeMonitor.Api +{ + [Route("api/ifttt/v1/test/setup")] + [ApiController] + public class TestSetupController : ControllerBase + { + // POST api/ifttt/v1/test/setup + [HttpPost] + public IActionResult Post() + { + ObjectResult failure = Fetcher.CheckIftttServiceKey(Request); + if (failure != null) + { + return failure; + } + + var result = new + { + data = new + { + samples = new + { + triggers = new + { + } + } + } + }; + + return Ok(result); + } + } +} diff --git a/OrcanodeMonitor/Core/Fetcher.cs b/OrcanodeMonitor/Core/Fetcher.cs index 3e5ada3..8042e67 100644 --- a/OrcanodeMonitor/Core/Fetcher.cs +++ b/OrcanodeMonitor/Core/Fetcher.cs @@ -13,6 +13,9 @@ using OrcanodeMonitor.Data; using Microsoft.IdentityModel.Tokens; using Mono.TextTemplating; +using Azure.Core; +using Microsoft.AspNetCore.Mvc; +using System.Net; namespace OrcanodeMonitor.Core { @@ -23,6 +26,9 @@ public class Fetcher private static string _orcasoundFeedsUrl = "https://live.orcasound.net/api/json/feeds"; private static string _dataplicityDevicesUrl = "https://apps.dataplicity.com/devices/"; private static DateTime _unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + private static string _iftttServiceKey = Environment.GetEnvironmentVariable("IFTTT_SERVICE_KEY") ?? ""; + + public static string IftttServiceKey => _iftttServiceKey; /// /// Test for a match between a human-readable name at Orcasound, and @@ -466,5 +472,27 @@ public async static Task UpdateManifestTimestampAsync(OrcanodeMonitorContext con AddOrcanodeStreamStatusEvent(context, node); } } + + /// + /// Check whether a request includes the correct IFTTT-Service-Key value. + /// + /// HTTP request received + /// null on success, ObjectResult if failed + public static ObjectResult? CheckIftttServiceKey(HttpRequest request) + { + if (request.Headers.TryGetValue("IFTTT-Service-Key", out var values) && + values.Any()) + { + string value = values.First(); + if (value == Fetcher.IftttServiceKey) + { + return null; + } + } + return new ObjectResult("Unauthorized access") + { + StatusCode = (int)HttpStatusCode.Unauthorized + }; + } } } diff --git a/OrcanodeMonitor/Models/Orcanode.cs b/OrcanodeMonitor/Models/Orcanode.cs index 0359dd5..5716b19 100644 --- a/OrcanodeMonitor/Models/Orcanode.cs +++ b/OrcanodeMonitor/Models/Orcanode.cs @@ -3,6 +3,9 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; +using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json.Linq; using OrcanodeMonitor.Core; namespace OrcanodeMonitor.Models @@ -20,6 +23,20 @@ public enum OrcanodeUpgradeStatus UpgradeAvailable } + public class OrcanodeIftttDTO + { + public OrcanodeIftttDTO(int id, string displayName) + { + ID = id; + DisplayName = displayName; + } + [JsonPropertyName("ID")] + public int ID { get; private set; } + [JsonPropertyName("display_name")] + public string DisplayName { get; private set; } + public override string ToString() => DisplayName; + } + public class Orcanode { const int _defaultMaxUploadDelayMinutes = 2; @@ -57,6 +74,8 @@ public Orcanode() DataplicitySerial = string.Empty; } + public OrcanodeIftttDTO ToIftttDTO() => new OrcanodeIftttDTO(ID, DisplayName); + /// /// The "serial" at Dataplicity. /// diff --git a/OrcanodeMonitor/Models/OrcanodeEvent.cs b/OrcanodeMonitor/Models/OrcanodeEvent.cs index 9d5b5cb..9b2be08 100644 --- a/OrcanodeMonitor/Models/OrcanodeEvent.cs +++ b/OrcanodeMonitor/Models/OrcanodeEvent.cs @@ -26,7 +26,7 @@ public OrcanodeEventIftttMeta(int id, DateTime timestamp) /// public class OrcanodeIftttEventDTO { - public OrcanodeIftttEventDTO(int id, string slug, string nodeName, string type, string value, DateTime timestamp) + public OrcanodeIftttEventDTO(int id, string nodeName, string slug, string type, string value, DateTime timestamp) { Slug = slug; Type = type; diff --git a/docs/Design.md b/docs/Design.md index e004d0e..78e1bc1 100644 --- a/docs/Design.md +++ b/docs/Design.md @@ -129,7 +129,7 @@ An IFTTT-compatible service can implement: The production server is online at: https://orcanodemonitor.azurewebsites.net/ -The production server is automatically updated if a Git tag with a [SemVer](semver.org) format version +The production server is automatically updated if a Git tag with a [SemVer](https://semver.org) format version (e.g., "v0.1.0") is pushed to GitHub. ### Staging server From 4f35470a590e8aa36603ac51eb8ee1bce58b2ba2 Mon Sep 17 00:00:00 2001 From: Dave Thaler Date: Tue, 11 Jun 2024 15:33:17 -0700 Subject: [PATCH 2/3] Fix IFTTT orcanodes query Signed-off-by: Dave Thaler --- .../Api/NodeStateEventsController.cs | 4 +- OrcanodeMonitor/Api/OrcanodesController.cs | 67 +++++++++++++++---- OrcanodeMonitor/Api/StatusController.cs | 4 +- OrcanodeMonitor/Api/TestSetupController.cs | 4 +- OrcanodeMonitor/Core/Fetcher.cs | 12 +++- OrcanodeMonitor/Models/OrcanodeEvent.cs | 4 +- 6 files changed, 70 insertions(+), 25 deletions(-) diff --git a/OrcanodeMonitor/Api/NodeStateEventsController.cs b/OrcanodeMonitor/Api/NodeStateEventsController.cs index f147aec..3a7c7d5 100644 --- a/OrcanodeMonitor/Api/NodeStateEventsController.cs +++ b/OrcanodeMonitor/Api/NodeStateEventsController.cs @@ -55,10 +55,10 @@ public IActionResult Get() [HttpPost] public IActionResult Post([FromBody] JsonElement requestBody) { - ObjectResult failure = Fetcher.CheckIftttServiceKey(Request); + var failure = Fetcher.CheckIftttServiceKey(Request); if (failure != null) { - return failure; + return Unauthorized(failure); } if (requestBody.ValueKind != JsonValueKind.Object) diff --git a/OrcanodeMonitor/Api/OrcanodesController.cs b/OrcanodeMonitor/Api/OrcanodesController.cs index ec8764b..daa6b62 100644 --- a/OrcanodeMonitor/Api/OrcanodesController.cs +++ b/OrcanodeMonitor/Api/OrcanodesController.cs @@ -7,12 +7,23 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; using OrcanodeMonitor.Core; using OrcanodeMonitor.Data; using OrcanodeMonitor.Models; namespace OrcanodeMonitor.Api { + public class ErrorResponse + { + public List Errors { get; set; } + } + + public class ErrorItem + { + public string Message { get; set; } + } + [Route("api/ifttt/v1/queries/[controller]")] [ApiController] public class OrcanodesController : ControllerBase @@ -30,7 +41,7 @@ public OrcanodesController(OrcanodeMonitorContext context) [HttpGet] public async Task>> GetOrcanode() { - return await GetJsonNodesAsync(_defaultLimit); + return await GetJsonNodesAsync(string.Empty, _defaultLimit); } #if false @@ -49,26 +60,49 @@ public async Task> GetOrcanode(int id) } #endif - private async Task GetJsonNodesAsync(int limit) + private async Task GetJsonNodesAsync(string cursor, int limit) { var nodes = await _databaseContext.Orcanodes.ToListAsync(); // Convert to IFTTT data transfer objects. var jsonNodes = new List(); + string myCursor = string.Empty; foreach (Orcanode node in nodes) { + string nodeCursor = node.ID.ToString(); + if (cursor != string.Empty) + { + if (cursor != nodeCursor) + { + continue; + } + // Found the node with the cursor so we can continue normally now. + cursor = string.Empty; + } + if (jsonNodes.Count >= limit) + { + myCursor = node.ID.ToString(); + break; + } jsonNodes.Add(node.ToIftttDTO()); } - var dataResult = new { data = jsonNodes }; - - var jsonString = JsonSerializer.Serialize(dataResult); - var jsonDocument = JsonDocument.Parse(jsonString); - - // Get the JSON data as an array. - var jsonElement = jsonDocument.RootElement; - - return new JsonResult(jsonElement); + if (!myCursor.IsNullOrEmpty()) + { + var dataResult = new { data = jsonNodes, cursor = myCursor }; + var jsonString = JsonSerializer.Serialize(dataResult); + var jsonDocument = JsonDocument.Parse(jsonString); + var jsonElement = jsonDocument.RootElement; + return new JsonResult(jsonElement); + } + else + { + var dataResult = new { data = jsonNodes }; + var jsonString = JsonSerializer.Serialize(dataResult); + var jsonDocument = JsonDocument.Parse(jsonString); + var jsonElement = jsonDocument.RootElement; + return new JsonResult(jsonElement); + } } // POST: api/ifttt/v1/queries/orcanodes @@ -76,10 +110,10 @@ private async Task GetJsonNodesAsync(int limit) [HttpPost] public async Task> PostOrcanode([FromBody] JsonElement requestBody) { - ObjectResult failure = Fetcher.CheckIftttServiceKey(Request); + var failure = Fetcher.CheckIftttServiceKey(Request); if (failure != null) { - return failure; + return Unauthorized(failure); } try @@ -92,12 +126,17 @@ public async Task> PostOrcanode([FromBody] JsonElement re limit = explicitLimit; } } + string cursor = string.Empty; + if (requestBody.TryGetProperty("cursor", out JsonElement cursorElement)) + { + cursor = cursorElement.ToString(); + } if (requestBody.TryGetProperty("queryFields", out JsonElement triggerFields)) { // TODO: use queryFields. } - return await GetJsonNodesAsync(limit); + return await GetJsonNodesAsync(cursor, limit); } catch (JsonException) { diff --git a/OrcanodeMonitor/Api/StatusController.cs b/OrcanodeMonitor/Api/StatusController.cs index ffac09d..c2b50e5 100644 --- a/OrcanodeMonitor/Api/StatusController.cs +++ b/OrcanodeMonitor/Api/StatusController.cs @@ -14,10 +14,10 @@ public class StatusController : ControllerBase [HttpGet] public IActionResult Get() { - ObjectResult failure = Fetcher.CheckIftttServiceKey(Request); + var failure = Fetcher.CheckIftttServiceKey(Request); if (failure != null) { - return failure; + return Unauthorized(failure); } return Ok(); } diff --git a/OrcanodeMonitor/Api/TestSetupController.cs b/OrcanodeMonitor/Api/TestSetupController.cs index fc467a7..d82191f 100644 --- a/OrcanodeMonitor/Api/TestSetupController.cs +++ b/OrcanodeMonitor/Api/TestSetupController.cs @@ -14,10 +14,10 @@ public class TestSetupController : ControllerBase [HttpPost] public IActionResult Post() { - ObjectResult failure = Fetcher.CheckIftttServiceKey(Request); + var failure = Fetcher.CheckIftttServiceKey(Request); if (failure != null) { - return failure; + return Unauthorized(failure); } var result = new diff --git a/OrcanodeMonitor/Core/Fetcher.cs b/OrcanodeMonitor/Core/Fetcher.cs index 8042e67..137c8f4 100644 --- a/OrcanodeMonitor/Core/Fetcher.cs +++ b/OrcanodeMonitor/Core/Fetcher.cs @@ -16,6 +16,7 @@ using Azure.Core; using Microsoft.AspNetCore.Mvc; using System.Net; +using OrcanodeMonitor.Api; namespace OrcanodeMonitor.Core { @@ -478,7 +479,7 @@ public async static Task UpdateManifestTimestampAsync(OrcanodeMonitorContext con /// /// HTTP request received /// null on success, ObjectResult if failed - public static ObjectResult? CheckIftttServiceKey(HttpRequest request) + public static ErrorResponse? CheckIftttServiceKey(HttpRequest request) { if (request.Headers.TryGetValue("IFTTT-Service-Key", out var values) && values.Any()) @@ -489,10 +490,15 @@ public async static Task UpdateManifestTimestampAsync(OrcanodeMonitorContext con return null; } } - return new ObjectResult("Unauthorized access") + string errorMessage = "Unauthorized access"; + var errorResponse = new ErrorResponse { - StatusCode = (int)HttpStatusCode.Unauthorized + Errors = new List + { + new ErrorItem { Message = errorMessage } + } }; + return errorResponse; } } } diff --git a/OrcanodeMonitor/Models/OrcanodeEvent.cs b/OrcanodeMonitor/Models/OrcanodeEvent.cs index 9b2be08..edc2466 100644 --- a/OrcanodeMonitor/Models/OrcanodeEvent.cs +++ b/OrcanodeMonitor/Models/OrcanodeEvent.cs @@ -46,8 +46,8 @@ public override string ToString() { return string.Format("{0} {1} {2} at {3}", Slug, Type, Value, Fetcher.UnixTimeStampToDateTimeLocal(Meta.UnixTimestamp)); } - [JsonPropertyName("timestamp")] - public DateTime? DateTime => Fetcher.UnixTimeStampToDateTimeLocal(Meta.UnixTimestamp); + [JsonPropertyName("created_at")] + public DateTime? CreatedAt => Fetcher.UnixTimeStampToDateTimeUtc(Meta.UnixTimestamp); [JsonPropertyName("description")] public string Description { get; private set; } } From 654b5a440dfff207c5d3be8a3d45ee008388fd59 Mon Sep 17 00:00:00 2001 From: Dave Thaler Date: Tue, 11 Jun 2024 16:09:07 -0700 Subject: [PATCH 3/3] Make Orcanode navigation property work on OrcanodeEvent class Signed-off-by: Dave Thaler --- OrcanodeMonitor/Data/OrcanodeMonitorContext.cs | 11 +++++++++++ OrcanodeMonitor/Models/OrcanodeEvent.cs | 2 +- OrcanodeMonitor/OrcanodeMonitor.csproj | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/OrcanodeMonitor/Data/OrcanodeMonitorContext.cs b/OrcanodeMonitor/Data/OrcanodeMonitorContext.cs index 2a7ead2..6e8378f 100644 --- a/OrcanodeMonitor/Data/OrcanodeMonitorContext.cs +++ b/OrcanodeMonitor/Data/OrcanodeMonitorContext.cs @@ -22,7 +22,18 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity().ToTable("Orcanode"); modelBuilder.Entity().ToTable("OrcanodeEvent"); + + modelBuilder.Entity() + .HasOne(e => e.Orcanode) // Navigation property + .WithMany() // Configure the inverse navigation property if needed + .HasForeignKey(e => e.OrcanodeId); // Foreign key + modelBuilder.Entity().ToTable("MonitorState"); } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseLazyLoadingProxies(); + // Other configuration... + } } } diff --git a/OrcanodeMonitor/Models/OrcanodeEvent.cs b/OrcanodeMonitor/Models/OrcanodeEvent.cs index edc2466..7c55a97 100644 --- a/OrcanodeMonitor/Models/OrcanodeEvent.cs +++ b/OrcanodeMonitor/Models/OrcanodeEvent.cs @@ -84,7 +84,7 @@ public OrcanodeEvent(Orcanode node, string type, string value, DateTime timestam public int OrcanodeId { get; set; } // Navigation property that uses OrcanodeId. - public Orcanode Orcanode { get; set; } + public virtual Orcanode Orcanode { get; set; } public string NodeName => Orcanode?.DisplayName ?? ""; diff --git a/OrcanodeMonitor/OrcanodeMonitor.csproj b/OrcanodeMonitor/OrcanodeMonitor.csproj index 685c78c..912256d 100644 --- a/OrcanodeMonitor/OrcanodeMonitor.csproj +++ b/OrcanodeMonitor/OrcanodeMonitor.csproj @@ -12,6 +12,7 @@ + all