From fc7bcfb06a0ea8052c0bbe7e93279d95c4b3c145 Mon Sep 17 00:00:00 2001 From: Dave Skender <8432125+DaveSkender@users.noreply.github.com> Date: Sun, 26 Jan 2025 00:12:42 -0500 Subject: [PATCH] refactor: services --- server/WebApi/Endpoints.cs | 4 +- server/WebApi/Program.cs | 7 ++ .../WebApi/{ => Services}/Service.Metadata.cs | 0 .../Service.Quotes.Failover.cs} | 72 +------------------ server/WebApi/Services/Service.Quotes.cs | 71 ++++++++++++++++++ .../WebApi/{ => Services}/Service.Storage.cs | 45 ++---------- .../WebApi/{ => Services}/StartupServices.cs | 0 7 files changed, 86 insertions(+), 113 deletions(-) rename server/WebApi/{ => Services}/Service.Metadata.cs (100%) rename server/WebApi/{Service.Quotes.cs => Services/Service.Quotes.Failover.cs} (97%) create mode 100644 server/WebApi/Services/Service.Quotes.cs rename server/WebApi/{ => Services}/Service.Storage.cs (53%) rename server/WebApi/{ => Services}/StartupServices.cs (100%) diff --git a/server/WebApi/Endpoints.cs b/server/WebApi/Endpoints.cs index dca1e38..dd86fdb 100644 --- a/server/WebApi/Endpoints.cs +++ b/server/WebApi/Endpoints.cs @@ -6,9 +6,9 @@ namespace WebApi.Controllers; [ApiController] [Route("")] -public class Main(QuoteService quoteService) : ControllerBase +public class Main(IQuoteService quoteService) : ControllerBase { - private readonly QuoteService quoteFeed = quoteService; + private readonly IQuoteService quoteFeed = quoteService; // GLOBALS private static readonly int limitLast = 120; diff --git a/server/WebApi/Program.cs b/server/WebApi/Program.cs index 03026c4..160500c 100644 --- a/server/WebApi/Program.cs +++ b/server/WebApi/Program.cs @@ -3,6 +3,7 @@ using System.IO.Compression; using Microsoft.AspNetCore.ResponseCompression; using WebApi.Services; +using Microsoft.Extensions.Azure; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); ConfigurationManager configuration = builder.Configuration; @@ -52,6 +53,12 @@ // Add logging services.AddLogging(); +// Add Azure dependencies +services.AddAzureClients(builder => { + builder.AddBlobServiceClient(configuration.GetValue("Storage:ConnectionString") + ?? "UseDevelopmentStorage=true"); +}); + // Add application services services.AddSingleton(); services.AddSingleton(); diff --git a/server/WebApi/Service.Metadata.cs b/server/WebApi/Services/Service.Metadata.cs similarity index 100% rename from server/WebApi/Service.Metadata.cs rename to server/WebApi/Services/Service.Metadata.cs diff --git a/server/WebApi/Service.Quotes.cs b/server/WebApi/Services/Service.Quotes.Failover.cs similarity index 97% rename from server/WebApi/Service.Quotes.cs rename to server/WebApi/Services/Service.Quotes.Failover.cs index c72eac8..314aea7 100644 --- a/server/WebApi/Service.Quotes.cs +++ b/server/WebApi/Services/Service.Quotes.Failover.cs @@ -1,76 +1,10 @@ -using System.Text.Json; - namespace WebApi.Services; -public interface IQuoteService -{ - Task> Get(); - Task> Get(string symbol); -} - -public class QuoteService( - ILogger logger, - IStorage storage) : IQuoteService +public partial class QuoteService { private static readonly IReadOnlyList backupQuotes = GetBackup().OrderBy(x => x.Date).ToList(); - private readonly ILogger _logger = logger; - private readonly IStorage _storage = storage; - - /// - /// Get default quotes - /// - /// List of default quotes - public async Task> Get() - => await Get("QQQ"); - - /// - /// Get quotes for a specific symbol. - /// - /// "SPY" or "QQQ" only, for now - public async Task> Get(string symbol) - { - string blobName = $"{symbol}-DAILY.json"; - - try - { - BlobClient blob = _storage.GetBlobClient(blobName); - - if (!await blob.ExistsAsync()) - { - _logger.LogWarning("Blob {BlobName} not found, using backup data", blobName); - return backupQuotes; - } - Response response = await blob.DownloadAsync(); - using Stream? stream = response?.Value.Content; - - if (stream == null) - { - _logger.LogError("Download stream was null for {BlobName}", blobName); - return backupQuotes; - } - - List? quotes = await JsonSerializer.DeserializeAsync>(stream); - - if (quotes == null || quotes.Count == 0) - { - _logger.LogWarning("No quotes found in {BlobName}", blobName); - return backupQuotes; - } - - return quotes; - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to retrieve quotes for {Symbol}", symbol); - return backupQuotes; - } - } - - #region Backup Quotes (for failover) - - private static List GetBackup() - => + private static List GetBackup() => [ new() { Date = DateTime.Parse("2018-12-31"), Open = 244.92m, High = 245.54m, Low = 242.87m, Close = 245.28m, Volume = 147031456 }, new() { Date = DateTime.Parse("2018-12-28"), Open = 244.94m, High = 246.73m, Low = 241.87m, Close = 243.15m, Volume = 155998912 }, @@ -577,7 +511,5 @@ private static List GetBackup() new() { Date = DateTime.Parse("2017-01-02"), Open = 212.61m, High = 213.35m, Low = 211.52m, Close = 212.00m, Volume = 76708880 }, new() { Date = DateTime.Parse("2017-01-01"), Open = 212.61m, High = 213.35m, Low = 211.52m, Close = 211.60m, Volume = 86708880 }, ]; - - #endregion } diff --git a/server/WebApi/Services/Service.Quotes.cs b/server/WebApi/Services/Service.Quotes.cs new file mode 100644 index 0000000..6e0c167 --- /dev/null +++ b/server/WebApi/Services/Service.Quotes.cs @@ -0,0 +1,71 @@ +using System.Text.Json; +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; +using Azure; + +namespace WebApi.Services; + +public interface IQuoteService +{ + Task> Get(); + Task> Get(string symbol); +} + +public partial class QuoteService( + ILogger logger, + IStorage storage) : IQuoteService +{ + private readonly ILogger _logger = logger; + private readonly IStorage _storage = storage; + + /// + /// Get default quotes + /// + /// List of default quotes + public async Task> Get() + => await Get("QQQ"); + + /// + /// Get quotes for a specific symbol. + /// + /// "SPY" or "QQQ" only, for now + public async Task> Get(string symbol) + { + string blobName = $"{symbol}-DAILY.json"; + + try + { + BlobClient blob = _storage.GetBlobClient(blobName); + + if (!await blob.ExistsAsync()) + { + _logger.LogWarning("Blob {BlobName} not found, using backup data", blobName); + return backupQuotes; + } + + Response response = await blob.DownloadAsync(); + using Stream? stream = response?.Value.Content; + + if (stream == null) + { + _logger.LogError("Download stream was null for {BlobName}", blobName); + return backupQuotes; + } + + List? quotes = await JsonSerializer.DeserializeAsync>(stream); + + if (quotes == null || quotes.Count == 0) + { + _logger.LogWarning("No quotes found in {BlobName}", blobName); + return backupQuotes; + } + + return quotes.OrderBy(x => x.Date); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to retrieve quotes for {Symbol}", symbol); + return backupQuotes; + } + } +} diff --git a/server/WebApi/Service.Storage.cs b/server/WebApi/Services/Service.Storage.cs similarity index 53% rename from server/WebApi/Service.Storage.cs rename to server/WebApi/Services/Service.Storage.cs index 348e13e..b563469 100644 --- a/server/WebApi/Service.Storage.cs +++ b/server/WebApi/Services/Service.Storage.cs @@ -11,14 +11,9 @@ public class Storage : IStorage { private readonly string _containerName; private readonly BlobContainerClient _blobClient; - private readonly ILogger _logger; - public Storage( - IConfiguration configuration, - ILogger logger) + public Storage(IConfiguration configuration) { - _logger = logger; - _containerName = configuration.GetValue("Storage:ContainerName") ?? "chart-demo"; @@ -38,22 +33,7 @@ string connectionString /// Task representing the async operation public async Task InitializeAsync(CancellationToken cancellationToken = default) { - try - { - _logger.LogInformation("Initializing blob storage for container {ContainerName}...", _containerName); - - Response response = await _blobClient.CreateIfNotExistsAsync(cancellationToken: cancellationToken); - string message = response?.Value != null - ? "Created new blob container" - : "Using existing blob container"; - - _logger.LogInformation("{Message} {ContainerName}", message, _containerName); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to initialize blob storage for {ContainerName}", _containerName); - throw; - } + await _blobClient.CreateIfNotExistsAsync(cancellationToken: cancellationToken); } /// @@ -64,25 +44,8 @@ public async Task InitializeAsync(CancellationToken cancellationToken = default) /// Task representing the async operation public async Task PutBlobAsync(string blobName, string content) { - try - { - BlobClient blob = GetBlobClient(blobName); - if (await blob.ExistsAsync()) - { - _logger.LogWarning("Overwriting existing blob {BlobName}", blobName); - } - - BlobHttpHeaders httpHeader = new() { ContentType = "application/json" }; - using MemoryStream ms = new(Encoding.UTF8.GetBytes(content)); - await blob.UploadAsync(ms, httpHeader); - - _logger.LogInformation("Successfully uploaded blob {BlobName}", blobName); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to upload blob {BlobName}", blobName); - throw; - } + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(content)); + await _blobClient.GetBlobClient(blobName).UploadAsync(stream, overwrite: true); } /// diff --git a/server/WebApi/StartupServices.cs b/server/WebApi/Services/StartupServices.cs similarity index 100% rename from server/WebApi/StartupServices.cs rename to server/WebApi/Services/StartupServices.cs