diff --git a/PocketDDD.BlazorClient/PocketDDD.BlazorClient/Services/FakePocketDDDApiService.cs b/PocketDDD.BlazorClient/PocketDDD.BlazorClient/Services/FakePocketDDDApiService.cs index 7bd31e8..0a2e9a3 100644 --- a/PocketDDD.BlazorClient/PocketDDD.BlazorClient/Services/FakePocketDDDApiService.cs +++ b/PocketDDD.BlazorClient/PocketDDD.BlazorClient/Services/FakePocketDDDApiService.cs @@ -25,7 +25,8 @@ public void SetUserAuthToken(string token) return new EventDataResponseDTO { - Version = 1, + Id = 1, + Version = 0, //Set to 0 so if we ever connect this to a real API then it will update! TimeSlots = new[] { new TimeSlotDTO diff --git a/PocketDDD.Server/PocketDDD.Server.DB/Migrations/2025_SeedData.sql b/PocketDDD.Server/PocketDDD.Server.DB/Migrations/2025_SeedData.sql index 6eb2316..e492487 100644 --- a/PocketDDD.Server/PocketDDD.Server.DB/Migrations/2025_SeedData.sql +++ b/PocketDDD.Server/PocketDDD.Server.DB/Migrations/2025_SeedData.sql @@ -8,18 +8,6 @@ delete TimeSlots delete Tracks delete EventDetail - -GO - --- Reset the identity columns -DBCC CHECKIDENT ('[Tracks]', RESEED, 0); -DBCC CHECKIDENT ('[TimeSlots]', RESEED, 0); -DBCC CHECKIDENT ('[Sessions]', RESEED, 0); - --- There is hardcoding to EventDetail ID 1 so we reset to 1 not 0 -DBCC CHECKIDENT ('[EventDetail]', RESEED, 1); -- Use if this is a brand new table that has never been used before ---DBCC CHECKIDENT ('[EventDetail]', RESEED, 0); -- Use if this is an empty table that used to have rows - GO -- Add 2025 Sessionize ID diff --git a/PocketDDD.Server/PocketDDD.Server.Services/EventDataService.cs b/PocketDDD.Server/PocketDDD.Server.Services/EventDataService.cs index 484f440..d719f04 100644 --- a/PocketDDD.Server/PocketDDD.Server.Services/EventDataService.cs +++ b/PocketDDD.Server/PocketDDD.Server.Services/EventDataService.cs @@ -1,63 +1,79 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; using PocketDDD.Server.DB; -using PocketDDD.Server.Model.DTOs; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using PocketDDD.Shared.API.RequestDTOs; using PocketDDD.Shared.API.ResponseDTOs; namespace PocketDDD.Server.Services; -public class EventDataService + +public class EventDataService(PocketDDDContext dbContext, IMemoryCache memoryCache, ILogger logger) { - private readonly PocketDDDContext dbContext; + private const string FetchLatestEventDataCacheKey = nameof(FetchLatestEventData); + private const string FetchCurrentEventDetailIdCacheKey = nameof(FetchCurrentEventDetailId); - public EventDataService(PocketDDDContext dbContext) + public async Task FetchCurrentEventDetailId() { - this.dbContext = dbContext; + if (memoryCache.TryGetValue(FetchCurrentEventDetailIdCacheKey, out var cachedEventDetailId)) + return cachedEventDetailId!.Value; + + cachedEventDetailId = await dbContext.EventDetail.MaxAsync(e => e.Id); + memoryCache.Set(FetchCurrentEventDetailIdCacheKey, cachedEventDetailId, TimeSpan.FromMinutes(5)); + + return cachedEventDetailId.Value; } - public async Task FetchLatestEventData(EventDataUpdateRequestDTO requestDTO) + public async Task FetchLatestEventData(EventDataUpdateRequestDTO requestDto) { - var eventDetails = await dbContext.EventDetail - .Include(x => x.TimeSlots) - .Include(x => x.Tracks) - .Include(x => x.Sessions) - .SingleAsync(x => x.Id == 1); - - if (requestDTO.Version == eventDetails!.Version) + var currentEventDetailId = await FetchCurrentEventDetailId(); + + if (memoryCache.TryGetValue(FetchLatestEventDataCacheKey, out var latestEventData)) + { + logger.LogDebug("Retrieved latest event data from the cache {eventData}", latestEventData); + } + else + { + logger.LogDebug("No event data in the cache, retrieving from the db"); + latestEventData = await dbContext.EventDetail + .AsNoTracking() + .AsSingleQuery() + .Select(eventDetails => new EventDataResponseDTO + { + Id = eventDetails.Id, + Version = eventDetails.Version, + TimeSlots = eventDetails.TimeSlots.Select(ts => new TimeSlotDTO + { + Id = ts.Id, + Info = ts.Info, + From = ts.From, + To = ts.To + }).ToList(), + Tracks = eventDetails.Tracks.Select(t => new TrackDTO + { + Id = t.Id, + Name = t.Name, + RoomName = t.RoomName + }).ToList(), + Sessions = eventDetails.Sessions.Select(s => new SessionDTO + { + Id = s.Id, + Title = s.Title, + ShortDescription = s.ShortDescription, + FullDescription = s.FullDescription, + Speaker = s.Speaker, + TimeSlotId = s.TimeSlot.Id, + TrackId = s.Track.Id + }).ToList() + }) + .SingleAsync(e => e.Id == currentEventDetailId); + + memoryCache.Set(FetchLatestEventDataCacheKey, latestEventData, TimeSpan.FromMinutes(5)); + logger.LogDebug("Updated the latest event data in the cache {eventData}", latestEventData); + } + + if (requestDto.Version >= latestEventData!.Version) return null; - var dtoResponse = new EventDataResponseDTO - { - Version = eventDetails.Version, - TimeSlots = eventDetails.TimeSlots.Select(ts => new TimeSlotDTO - { - Id = ts.Id, - Info = ts.Info, - From = ts.From, - To = ts.To - }).ToList(), - Tracks = eventDetails.Tracks.Select(t => new TrackDTO - { - Id = t.Id, - Name = t.Name, - RoomName = t.RoomName - }).ToList(), - Sessions = eventDetails.Sessions.Select(s => new SessionDTO - { - Id = s.Id, - Title = s.Title, - ShortDescription = s.ShortDescription, - FullDescription = s.FullDescription, - Speaker = s.Speaker, - TimeSlotId = s.TimeSlot.Id, - TrackId = s.Track.Id - }).ToList() - }; - - return dtoResponse; + return latestEventData; } } diff --git a/PocketDDD.Server/PocketDDD.Server.Services/RegistrationService.cs b/PocketDDD.Server/PocketDDD.Server.Services/RegistrationService.cs index 0cf91c9..eb29e10 100644 --- a/PocketDDD.Server/PocketDDD.Server.Services/RegistrationService.cs +++ b/PocketDDD.Server/PocketDDD.Server.Services/RegistrationService.cs @@ -1,30 +1,20 @@ -using PocketDDD.Server.DB; +using System.Security.Cryptography; +using PocketDDD.Server.DB; using PocketDDD.Server.Model.DBModel; -using PocketDDD.Server.Model.DTOs; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Cryptography; -using System.Text; -using System.Threading.Tasks; using PocketDDD.Shared.API.RequestDTOs; using PocketDDD.Shared.API.ResponseDTOs; namespace PocketDDD.Server.Services; -public class RegistrationService -{ - private readonly PocketDDDContext dbContext; - - public RegistrationService(PocketDDDContext dbContext) - { - this.dbContext = dbContext; - } +public class RegistrationService(PocketDDDContext dbContext, EventDataService eventDataService) +{ public async Task Register(RegisterDTO dto) { + var currentEventDetailId = await eventDataService.FetchCurrentEventDetailId(); + var user = new User { - EventDetailId = 1, + EventDetailId = currentEventDetailId, Name = dto.Name, Token = GenerateBearerToken(), EventScore = 1 diff --git a/PocketDDD.Server/PocketDDD.Server.Services/SessionizeService.cs b/PocketDDD.Server/PocketDDD.Server.Services/SessionizeService.cs index 3f0a9ee..bba0b3e 100644 --- a/PocketDDD.Server/PocketDDD.Server.Services/SessionizeService.cs +++ b/PocketDDD.Server/PocketDDD.Server.Services/SessionizeService.cs @@ -1,34 +1,46 @@ -using Microsoft.EntityFrameworkCore; +using System.Net.Http.Json; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using PocketDDD.Server.DB; using PocketDDD.Server.Model.DBModel; using PocketDDD.Server.Model.Sessionize; -using System.Net.Http.Json; +using Session = PocketDDD.Server.Model.DBModel.Session; namespace PocketDDD.Server.Services; public class SessionizeService { - private readonly HttpClient httpClient; private readonly PocketDDDContext dbContext; + private readonly HttpClient httpClient; - public SessionizeService(HttpClient httpClient, PocketDDDContext dbContext) + public SessionizeService(HttpClient httpClient, PocketDDDContext dbContext, ILogger logger) { + Logger = logger; this.httpClient = httpClient; this.dbContext = dbContext; httpClient.BaseAddress = new Uri("https://sessionize.com/api/v2/"); } + private ILogger Logger { get; } + public async Task UpdateFromSessionize() { - var dbEvent = await dbContext.EventDetail.SingleAsync(x => x.Id == 1); + Logger.LogInformation("Looking for event detail in database"); + var dbEvent = await dbContext.EventDetail.OrderBy(x => x.Id).LastAsync(); var sessionizeEventId = dbEvent.SessionizeId; + + Logger.LogInformation("About to get data from Sessionize API"); + var sessionizeEvent = await httpClient.GetFromJsonAsync($"{sessionizeEventId}/view/All"); if (sessionizeEvent is null) throw new ArgumentNullException(nameof(sessionizeEvent)); - var dbTracks = await dbContext.Tracks.ToListAsync(); + Logger.LogInformation("Information retrieved from Sessionize API"); + Logger.LogInformation("Looking for changes to rooms"); + + var dbTracks = await dbContext.Tracks.Where(track => track.EventDetail.Id == dbEvent.Id).ToListAsync(); foreach (var item in sessionizeEvent.rooms) { var dbTrack = dbTracks.SingleOrDefault(x => x.SessionizeId == item.id); @@ -46,10 +58,21 @@ public async Task UpdateFromSessionize() dbTrack.Name = $"Track {item.sort}"; } - await dbContext.SaveChangesAsync(); + if (dbContext.ChangeTracker.HasChanges()) + { + dbEvent.Version++; + Logger.LogInformation("Updating db with changes to rooms"); + await dbContext.SaveChangesAsync(); + } + else + { + Logger.LogInformation("No changes to rooms were detected"); + } + Logger.LogInformation("Looking for changes to time slots and breaks"); - var dbTimeSlots = await dbContext.TimeSlots.ToListAsync(); + var dbTimeSlots = await dbContext.TimeSlots.Where(timeSlot => timeSlot.EventDetail.Id == dbEvent.Id) + .ToListAsync(); var sessionizeTimeSlots = sessionizeEvent.sessions .Select(x => (x.startsAt, x.endsAt, x.isServiceSession, serviceSessionDetails: x.isServiceSession ? x.title : null)) @@ -65,17 +88,28 @@ public async Task UpdateFromSessionize() { EventDetail = dbEvent, From = item.startsAt, - To = item.endsAt, - Info = item.isServiceSession ? item.serviceSessionDetails : null + To = item.endsAt }; dbContext.TimeSlots.Add(dbTimeSlot); } + + dbTimeSlot.Info = item.isServiceSession ? item.serviceSessionDetails : null; } - await dbContext.SaveChangesAsync(); + if (dbContext.ChangeTracker.HasChanges()) + { + dbEvent.Version++; + Logger.LogInformation("Updating db with changes to time slots and breaks"); + await dbContext.SaveChangesAsync(); + } + else + { + Logger.LogInformation("No changes to time slots or breaks were detected"); + } + Logger.LogInformation("Looking for changes to sessions"); - var dbSessions = await dbContext.Sessions.ToListAsync(); + var dbSessions = await dbContext.Sessions.Where(session => session.EventDetail.Id == dbEvent.Id).ToListAsync(); var speakers = sessionizeEvent.speakers; dbTracks = await dbContext.Tracks.ToListAsync(); dbTimeSlots = await dbContext.TimeSlots.ToListAsync(); @@ -89,7 +123,7 @@ public async Task UpdateFromSessionize() var dbSession = dbSessions.SingleOrDefault(x => x.SessionizeId == sessionizeId); if (dbSession == null) { - dbSession = new Model.DBModel.Session + dbSession = new Session { SessionizeId = sessionizeId, EventDetail = dbEvent, @@ -106,7 +140,16 @@ public async Task UpdateFromSessionize() dbSession.TimeSlot = GetTimeSlot(dbTimeSlots, item.startsAt, item.endsAt); } - await dbContext.SaveChangesAsync(); + if (dbContext.ChangeTracker.HasChanges()) + { + dbEvent.Version++; + Logger.LogInformation("Updating db with changes to sessions"); + await dbContext.SaveChangesAsync(); + } + else + { + Logger.LogInformation("No changes to sessions were detected"); + } } private string GetSpeakers(List speakers, List speakerIds) diff --git a/PocketDDD.Server/PocketDDD.Server.WebAPI/Program.cs b/PocketDDD.Server/PocketDDD.Server.WebAPI/Program.cs index 6bb707f..329e43f 100644 --- a/PocketDDD.Server/PocketDDD.Server.WebAPI/Program.cs +++ b/PocketDDD.Server/PocketDDD.Server.WebAPI/Program.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using PocketDDD.Server.DB; using PocketDDD.Server.Services; +using PocketDDD.Server.WebAPI; using PocketDDD.Server.WebAPI.Authentication; var corsPolicy = "corsPolicy"; @@ -28,6 +29,8 @@ options => options.UseSqlServer("name=ConnectionStrings:PocketDDDContext") ); +builder.Services.AddMemoryCache(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -37,6 +40,8 @@ builder.Services.AddHttpClient(); +builder.Services.AddHostedService(); + builder.Services.AddAuthentication() .AddScheme(UserIsRegisteredAuthHandler.SchemeName, null); diff --git a/PocketDDD.Server/PocketDDD.Server.WebAPI/UpdateFromSessionizeBackgroundService.cs b/PocketDDD.Server/PocketDDD.Server.WebAPI/UpdateFromSessionizeBackgroundService.cs new file mode 100644 index 0000000..6d80a80 --- /dev/null +++ b/PocketDDD.Server/PocketDDD.Server.WebAPI/UpdateFromSessionizeBackgroundService.cs @@ -0,0 +1,37 @@ +using PocketDDD.Server.Services; + +namespace PocketDDD.Server.WebAPI; + +public class UpdateFromSessionizeBackgroundService( + IServiceProvider services, + ILogger logger) + : BackgroundService +{ + private ILogger Logger { get; } = logger; + private IServiceProvider Services { get; } = services; + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + Logger.LogInformation("Update from Sessionize background task started."); + + while (!stoppingToken.IsCancellationRequested) + { + Logger.LogInformation("About to update from Sessionize."); + try + { + using var scope = Services.CreateScope(); + + var sessionizeService = scope.ServiceProvider.GetRequiredService(); + await sessionizeService.UpdateFromSessionize(); + + Logger.LogInformation("Update from Sessionize complete."); + } + catch (Exception e) + { + Logger.LogError(e, "Update from Sessionize failed."); + } + + await Task.Delay(TimeSpan.FromMinutes(30), stoppingToken); + } + } +} \ No newline at end of file diff --git a/PocketDDD.Shared/API/ResponseDTOs/EventDataResponseDTO.cs b/PocketDDD.Shared/API/ResponseDTOs/EventDataResponseDTO.cs index d82a5bb..f1344c2 100644 --- a/PocketDDD.Shared/API/ResponseDTOs/EventDataResponseDTO.cs +++ b/PocketDDD.Shared/API/ResponseDTOs/EventDataResponseDTO.cs @@ -1,12 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace PocketDDD.Shared.API.ResponseDTOs; -namespace PocketDDD.Shared.API.ResponseDTOs; public record EventDataResponseDTO { + public int Id { get; init; } public int Version { get; set; } public IEnumerable TimeSlots { get; set; } = Enumerable.Empty(); public IEnumerable Tracks { get; set; } = Enumerable.Empty(); @@ -37,4 +33,4 @@ public class SessionDTO public string Speaker { get; set; } = ""; public int TrackId { get; set; } public int TimeSlotId { get; set; } -} +} \ No newline at end of file