Skip to content

Commit

Permalink
refactor: services
Browse files Browse the repository at this point in the history
  • Loading branch information
DaveSkender committed Jan 26, 2025
1 parent f383e2f commit fc7bcfb
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 113 deletions.
4 changes: 2 additions & 2 deletions server/WebApi/Endpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 7 additions & 0 deletions server/WebApi/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -52,6 +53,12 @@
// Add logging
services.AddLogging();

// Add Azure dependencies
services.AddAzureClients(builder => {
builder.AddBlobServiceClient(configuration.GetValue<string>("Storage:ConnectionString")
?? "UseDevelopmentStorage=true");
});

// Add application services
services.AddSingleton<IStorage, Storage>();
services.AddSingleton<IQuoteService, QuoteService>();
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,76 +1,10 @@
using System.Text.Json;

namespace WebApi.Services;

public interface IQuoteService
{
Task<IEnumerable<Quote>> Get();
Task<IEnumerable<Quote>> Get(string symbol);
}

public class QuoteService(
ILogger<QuoteService> logger,
IStorage storage) : IQuoteService
public partial class QuoteService

Check notice on line 3 in server/WebApi/Services/Service.Quotes.Failover.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

server/WebApi/Services/Service.Quotes.Failover.cs#L3

'partial' is gratuitous in this context.

Check warning on line 3 in server/WebApi/Services/Service.Quotes.Failover.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

server/WebApi/Services/Service.Quotes.Failover.cs#L3

Add a 'protected' constructor or the 'static' keyword to the class declaration.
{
private static readonly IReadOnlyList<Quote> backupQuotes = GetBackup().OrderBy(x => x.Date).ToList();
private readonly ILogger<QuoteService> _logger = logger;
private readonly IStorage _storage = storage;

/// <summary>
/// Get default quotes
/// </summary>
/// <returns cref="Quote">List of default quotes</returns>
public async Task<IEnumerable<Quote>> Get()
=> await Get("QQQ");

/// <summary>
/// Get quotes for a specific symbol.
/// </summary>
/// <param name="symbol">"SPY" or "QQQ" only, for now</param>
public async Task<IEnumerable<Quote>> 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<BlobDownloadInfo> 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<Quote>? quotes = await JsonSerializer.DeserializeAsync<List<Quote>>(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<Quote> GetBackup()
=>
private static List<Quote> 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 },
Expand Down Expand Up @@ -577,7 +511,5 @@ private static List<Quote> 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
}

71 changes: 71 additions & 0 deletions server/WebApi/Services/Service.Quotes.cs
Original file line number Diff line number Diff line change
@@ -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<IEnumerable<Quote>> Get();
Task<IEnumerable<Quote>> Get(string symbol);
}

public partial class QuoteService(

Check notice on line 14 in server/WebApi/Services/Service.Quotes.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

server/WebApi/Services/Service.Quotes.cs#L14

'partial' is gratuitous in this context.
ILogger<QuoteService> logger,
IStorage storage) : IQuoteService
{
private readonly ILogger<QuoteService> _logger = logger;
private readonly IStorage _storage = storage;

/// <summary>
/// Get default quotes
/// </summary>
/// <returns cref="Quote">List of default quotes</returns>
public async Task<IEnumerable<Quote>> Get()
=> await Get("QQQ");

/// <summary>
/// Get quotes for a specific symbol.
/// </summary>
/// <param name="symbol">"SPY" or "QQQ" only, for now</param>
public async Task<IEnumerable<Quote>> 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<BlobDownloadInfo> 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<Quote>? quotes = await JsonSerializer.DeserializeAsync<List<Quote>>(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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,9 @@ public class Storage : IStorage
{
private readonly string _containerName;

Check notice on line 12 in server/WebApi/Services/Service.Storage.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

server/WebApi/Services/Service.Storage.cs#L12

Remove the field '_containerName' and declare it as a local variable in the relevant methods.
private readonly BlobContainerClient _blobClient;
private readonly ILogger<Storage> _logger;

public Storage(
IConfiguration configuration,
ILogger<Storage> logger)
public Storage(IConfiguration configuration)
{
_logger = logger;

_containerName
= configuration.GetValue<string>("Storage:ContainerName")
?? "chart-demo";
Expand All @@ -38,22 +33,7 @@ string connectionString
/// <returns>Task representing the async operation</returns>
public async Task InitializeAsync(CancellationToken cancellationToken = default)
{
try
{
_logger.LogInformation("Initializing blob storage for container {ContainerName}...", _containerName);

Response<BlobContainerInfo> 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);
}

/// <summary>
Expand All @@ -64,25 +44,8 @@ public async Task InitializeAsync(CancellationToken cancellationToken = default)
/// <returns>Task representing the async operation</returns>
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);
}

/// <summary>
Expand Down
File renamed without changes.

0 comments on commit fc7bcfb

Please sign in to comment.