diff --git a/.gitignore b/.gitignore index 6020982..c661872 100644 --- a/.gitignore +++ b/.gitignore @@ -197,4 +197,5 @@ dependency-reduced-pom.xml README.html *.iml .idea -.exercism \ No newline at end of file +.exercism +/src/.vs diff --git a/src/Backend/AppContext.cs b/src/Backend/AppContext.cs deleted file mode 100644 index 8d97ddf..0000000 --- a/src/Backend/AppContext.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Backend.Operations; -using Microsoft.Azure.Cosmos; - -namespace Backend -{ - public class AppContext - { - public readonly Operations.DataProvider dataProvider; - public readonly IConfiguration configuration; - public readonly ILogger logger; - - public AppContext(CosmosClient cosmosClient, IConfiguration configuration, ILogger logger) - { - this.dataProvider = new Operations.DataProvider(cosmosClient, configuration, logger); - this.configuration = configuration; - this.logger = logger; - } - } -} \ No newline at end of file diff --git a/src/Backend/Backend.csproj b/src/Backend/Backend.csproj index 15e787a..1a02122 100644 --- a/src/Backend/Backend.csproj +++ b/src/Backend/Backend.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -8,14 +8,18 @@ - + - + + + + + diff --git a/src/Backend/Backend.csproj.user b/src/Backend/Backend.csproj.user new file mode 100644 index 0000000..9ff5820 --- /dev/null +++ b/src/Backend/Backend.csproj.user @@ -0,0 +1,6 @@ + + + + https + + \ No newline at end of file diff --git a/src/Backend/Controllers/ProblemsController.cs b/src/Backend/Controllers/ProblemsController.cs index 7803a9c..7be6d7c 100644 --- a/src/Backend/Controllers/ProblemsController.cs +++ b/src/Backend/Controllers/ProblemsController.cs @@ -1,21 +1,24 @@ namespace Backend.Controllers { + using Backend.Filters; using Backend.Models.Public; using Backend.Operations; + using Common.Models; using Microsoft.AspNetCore.Mvc; - using ProblemPublicModel = Common.Models.Problem; [ApiController] [Route("api")] public class ProblemsController : ControllerBase { - private AppContext appContext; private readonly ILogger logger; private readonly IConfiguration configuration; - public ProblemsController(AppContext appContext, ILogger logger, IConfiguration configuration) + private readonly DataProvider dataProvider; + public ProblemsController(ILogger logger, + DataProvider dataProvider, + IConfiguration configuration) { - this.appContext = appContext; this.logger = logger; + this.dataProvider = dataProvider; this.configuration = configuration; } @@ -28,23 +31,23 @@ public ActionResult GetHome() [HttpGet] [Route("problems")] - public async Task>> GetProblems( + public async Task>> GetProblems( [FromQuery(Name = QueryParam.Skip)] int skip = 0, [FromQuery(Name = QueryParam.Limit)] int limit = 50, [FromQuery(Name = QueryParam.Company)] List? companies = null, - [FromQuery(Name = QueryParam.Difficulty)] List? difficulties = null, + [FromQuery(Name = QueryParam.Difficulty)] List? difficulties = null, [FromQuery(Name = QueryParam.Tag)] List? tags = null) { var filter = new ProblemFilter(skip, limit, companies, difficulties, tags); - var filteredProblems = await appContext.dataProvider.GetProblemsAsync(filter); + var filteredProblems = await dataProvider.GetProblemsAsync(filter); return Ok(filteredProblems); } [HttpGet] [Route("problems/{id}")] - public async Task> GetProblems(string id) + public async Task> GetProblems(string id) { - var problem = await appContext.dataProvider.GetProblemByIdAsync(id); + var problem = await dataProvider.GetProblemByIdAsync(id); if (problem != null) { return Ok(problem); diff --git a/src/Backend/Docs/NextPlanOfAction.md b/src/Backend/Docs/NextPlanOfAction.md new file mode 100644 index 0000000..c782975 --- /dev/null +++ b/src/Backend/Docs/NextPlanOfAction.md @@ -0,0 +1,8 @@ +## Next Plans +1. **Create functions for async updates for Jobs backend** +2. **Integrate Redis for fast retreival** +3. **Notification System upon new Job post arrival** + 3.1. **Will require integration to some async framework.** + 3.2. **Using Servicebus can be costly might use some other message broker** +4. **Custom tracing framework using Prometheus and Grafana (learning purpose)** +5. **Make the system scalable enough to register a new scraping source easily** \ No newline at end of file diff --git a/src/Backend/Filters/IFilter.cs b/src/Backend/Filters/IFilter.cs new file mode 100644 index 0000000..91a29e5 --- /dev/null +++ b/src/Backend/Filters/IFilter.cs @@ -0,0 +1,9 @@ +using Common.Models; + +namespace Backend.Filters +{ + public interface IFilter + { + public List ApplyFilterAsync(List problems); + } +} \ No newline at end of file diff --git a/src/Backend/Operations/ProblemFilter.cs b/src/Backend/Filters/ProblemFilter.cs similarity index 50% rename from src/Backend/Operations/ProblemFilter.cs rename to src/Backend/Filters/ProblemFilter.cs index c8d9535..a4b66f5 100644 --- a/src/Backend/Operations/ProblemFilter.cs +++ b/src/Backend/Filters/ProblemFilter.cs @@ -1,39 +1,41 @@ -namespace Backend.Operations +using Common.Models; + +namespace Backend.Filters { public class ProblemFilter : IFilter { private int skip = 0; private int limit = 50; private List companies; - private List difficulties; + private List difficulties; private List tags; - public ProblemFilter(int skip, int limit, List? companies, List? difficulties, List? tags) + public ProblemFilter(int skip, int limit, List? companies, List? difficulties, List? tags) { this.skip = skip; this.limit = Math.Min(limit, 50); this.companies = companies ?? new List(); - this.difficulties = difficulties ?? new List(); + this.difficulties = difficulties ?? new List(); this.tags = tags ?? new List(); } - public List ApplyFilterAsync(List problems) + public List ApplyFilterAsync(List problems) { - List filteredProblems = problems; + List filteredProblems = problems; // TODO: Add tags filtering logic with company - if ((companies != null && companies.Count > 0) || (tags != null && tags.Count > 0)) + if (companies != null && companies.Count > 0 || tags != null && tags.Count > 0) { filteredProblems = filteredProblems.Where( p => p.companies != null && p.companies.Any(kv => - (this.companies == null || this.companies.Count== 0 || this.companies.Contains(kv.Key, StringComparer.OrdinalIgnoreCase)) && - kv.Value.Any(t => this.tags == null || this.tags.Count == 0 || this.tags.Contains(t, StringComparer.OrdinalIgnoreCase)))).ToList(); + (companies == null || companies.Count== 0 || companies.Contains(kv.Key, StringComparer.OrdinalIgnoreCase)) && + kv.Value.Any(t => tags == null || tags.Count == 0 || tags.Contains(t, StringComparer.OrdinalIgnoreCase)))).ToList(); } if (difficulties != null && difficulties.Count > 0) { - filteredProblems = filteredProblems.Where(p => this.difficulties.Contains(p.difficulty)).ToList(); + filteredProblems = filteredProblems.Where(p => difficulties.Contains(p.difficulty)).ToList(); } filteredProblems = filteredProblems.Skip(skip).Take(limit).ToList(); diff --git a/src/Backend/Models/Internal/Problem.cs b/src/Backend/Models/Internal/Problem.cs deleted file mode 100644 index e69de29..0000000 diff --git a/src/Backend/Models/Public/Problem.cs b/src/Backend/Models/Public/Problem.cs deleted file mode 100644 index 0f6f34b..0000000 --- a/src/Backend/Models/Public/Problem.cs +++ /dev/null @@ -1 +0,0 @@ -// Using Common.Models.Problem scehma \ No newline at end of file diff --git a/src/Backend/Operations/DataProvider.cs b/src/Backend/Operations/DataProvider.cs index 8ed9d78..c280dd1 100644 --- a/src/Backend/Operations/DataProvider.cs +++ b/src/Backend/Operations/DataProvider.cs @@ -1,24 +1,18 @@ namespace Backend.Operations { + using Backend.Filters; + using Common.Cache; + using Common.Constants; using Common.Models; - using Microsoft.Azure.Cosmos; public class DataProvider { - private const int RefreshIntervalInHours = 3; - private CosmosClient cosmosClient; - private readonly IConfiguration configuration; - ILogger logger; - private DateTime lastLoadedTime = DateTime.MinValue; - private Task backgroundRefreshTask; - private Dictionary problemsCache = new Dictionary(StringComparer.OrdinalIgnoreCase); - - public DataProvider(CosmosClient client, IConfiguration configuration, ILogger logger) + private ICache _problemCache; + private ILogger _logger; + public DataProvider([FromKeyedServices(CacheConstants.ProblemCacheKey)] ICache problemCache, ILogger logger) { - this.cosmosClient = client; - this.configuration = configuration; - this.logger = logger; - this.backgroundRefreshTask = Task.Run(() => this.StartBackgroundRefreshAsync(CancellationToken.None)); + _problemCache = problemCache; + _logger = logger; } public async Task> GetProblemsAsync(IFilter? filter = null) @@ -43,69 +37,17 @@ public async Task> GetProblemsAsync(IFilter? filter = null) private async Task> GetAllProblemsAsync() { - if (problemsCache.Count == 0) + if (_problemCache.Contains(CacheConstants.ProblemCacheKey)) { - await LoadLatestDataAsync(); - if (problemsCache.Count == 0) - { - this.logger.LogWarning("No problems found in the cache after loading data."); - } + _logger.LogInformation("Problem cache hit. Retrieving data from cache."); } - return problemsCache; - } - - private async Task LoadLatestDataAsync() - { - int maxRetries = 3; - for(int i = 0; i < maxRetries; i++) + else { - try - { - var dbId = configuration.GetValue("ApplicationSettings:CosmosDbDatabaseId"); - var containerId = configuration.GetValue("ApplicationSettings:CosmosDbContainerId"); - var db = cosmosClient.GetDatabase(dbId); - var container = db.GetContainer(containerId); - - var query = "SELECT * FROM c"; - var queryDefinition = new QueryDefinition(query); - var queryResultSetIterator = container.GetItemQueryIterator(queryDefinition); - - List results = new List(); - while (queryResultSetIterator.HasMoreResults) - { - var response = await queryResultSetIterator.ReadNextAsync(); - results.AddRange(response.Select(item => new Problem(item))); - } - - lastLoadedTime = DateTime.UtcNow; - results = results.OrderBy(p => int.TryParse(p.id, out int id) ? id : -1).ToList(); - problemsCache = results.ToDictionary(p => p.id, StringComparer.OrdinalIgnoreCase); - this.logger.LogInformation($"Loaded {problemsCache.Count} problems from Cosmos DB at {lastLoadedTime}"); - break; - } - catch (Exception ex) - { - this.logger.LogError($"Error loading data from Cosmos DB. {ex}"); - await Task.Delay(TimeSpan.FromSeconds(2 * (i + 1))); - } - } - } - - public async Task StartBackgroundRefreshAsync(CancellationToken cancellationToken) - { - while (!cancellationToken.IsCancellationRequested) - { - try - { - await LoadLatestDataAsync(); - } - catch (Exception ex) - { - this.logger.LogError($"Error during background data refresh: {ex}"); - } - - await Task.Delay(TimeSpan.FromHours(RefreshIntervalInHours), cancellationToken); + _logger.LogInformation("Problem cache miss. Loading data into cache."); + await _problemCache.Populate(); } + + return _problemCache.Get>(CacheConstants.ProblemCacheKey) ?? new Dictionary(StringComparer.OrdinalIgnoreCase); } } } \ No newline at end of file diff --git a/src/Backend/Operations/IFilter.cs b/src/Backend/Operations/IFilter.cs deleted file mode 100644 index 2a2780a..0000000 --- a/src/Backend/Operations/IFilter.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Backend.Operations -{ - public interface IFilter - { - public List ApplyFilterAsync(List problems); - } -} \ No newline at end of file diff --git a/src/Backend/Program.cs b/src/Backend/Program.cs index e2c1fe5..3c19749 100644 --- a/src/Backend/Program.cs +++ b/src/Backend/Program.cs @@ -1,9 +1,13 @@ namespace Backend; -using System.Data; using Backend.Operations; +using Common.Cache; +using Common.Constants; +using Common.Factories; +using Common.Repositories; using Microsoft.Azure.Cosmos; using Microsoft.Extensions.Logging.ApplicationInsights; +using System.Data; public class Program { @@ -33,6 +37,7 @@ private static void SetupLogging(ILoggingBuilder logging, IConfiguration configu public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); + var services = builder.Services; // Add services to the container. builder.Services.AddApplicationInsightsTelemetry(); @@ -61,14 +66,33 @@ public static void Main(string[] args) // Register AppContext as singleton var config = builder.Configuration; - var cosmosClient = new CosmosClient(config["ApplicationSettings:CosmosDbUri"], config["ApplicationSettings:CosmosDbPrimaryKey"]); - builder.Services.AddSingleton(s => - new AppContext( - cosmosClient, - builder.Configuration, - s.GetRequiredService>() - ) - ); + + #region Register Cosmos related services + services.AddSingleton(s => + { + var cosmosDbUri = config[ConfigurationConstants.CosmosDBUriKey]; + var cosmosDbAccountKey = config[ConfigurationConstants.CosmosDBAccountKey]; + if (string.IsNullOrEmpty(cosmosDbUri) || string.IsNullOrEmpty(cosmosDbAccountKey)) + { + throw new DataException("Cosmos DB configuration is missing or invalid."); + } + return new CosmosClient(cosmosDbUri, cosmosDbAccountKey); + }); + + services.AddTransient(); + #endregion + + #region Register Cache + services.AddKeyedSingleton(CacheConstants.ProblemCacheKey); + #endregion + + #region Register Repositories + services.AddTransient(); + #endregion + + #region Register Miscellaneous Services + services.AddTransient(); + #endregion var app = builder.Build(); ILogger logger = app.Logger; diff --git a/src/Backend/appsettings.json b/src/Backend/appsettings.json index 9ee91c9..22c42e9 100644 --- a/src/Backend/appsettings.json +++ b/src/Backend/appsettings.json @@ -9,9 +9,9 @@ "Name": "leetcode-wrapper", "Version": "1.0.0", "CosmosDbUri": "https://lcw-cosmos.documents.azure.com:443/", - "CosmosDbPrimaryKey": "", - "CosmosDbDatabaseId": "LeetCodeWrapper", - "CosmosDbContainerId": "Problems" + "AccountKey": "", + "LCProject:DatabaseName": "LeetCodeWrapper", + "LCProject:ContainerName": "Problems" }, "ApplicationInsights": { "LogLevel": { diff --git a/src/Common/Cache/BaseCache.cs b/src/Common/Cache/BaseCache.cs new file mode 100644 index 0000000..5337506 --- /dev/null +++ b/src/Common/Cache/BaseCache.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; + +namespace Common.Cache +{ + public abstract class BaseCache : ICache + { + // Internal cache storage + protected readonly ConcurrentDictionary _cache = new(); + + // Represents a cache item with optional expiration + protected class CacheItem + { + public object? Value { get; set; } + public DateTime? Expiration { get; set; } + } + + public T? Get(string key) + { + if (_cache.TryGetValue(key, out var item)) + { + if (item.Expiration == null || item.Expiration > DateTime.UtcNow) + { + return item.Value is T value ? value : default; + } + // Item expired, remove it + _cache.TryRemove(key, out _); + } + return default; + } + + public void Set(string key, T value, TimeSpan? absoluteExpiration = null) + { + var expiration = absoluteExpiration.HasValue + ? DateTime.UtcNow.Add(absoluteExpiration.Value) + : (DateTime?)null; + + _cache[key] = new CacheItem + { + Value = value, + Expiration = expiration + }; + } + + public void Remove(string key) + { + _cache.TryRemove(key, out _); + } + + public bool Contains(string key) + { + if (_cache.TryGetValue(key, out var item)) + { + if (item.Expiration == null || item.Expiration > DateTime.UtcNow) + { + return true; + } + // Item expired, remove it + _cache.TryRemove(key, out _); + } + return false; + } + + /// + /// Populates the cache using a custom technique. + /// Override this method in derived classes for custom population logic. + /// + public abstract Task Populate(); + } +} \ No newline at end of file diff --git a/src/Common/Cache/ICache.cs b/src/Common/Cache/ICache.cs new file mode 100644 index 0000000..56d36d2 --- /dev/null +++ b/src/Common/Cache/ICache.cs @@ -0,0 +1,42 @@ +using System; + +namespace Common.Cache +{ + public interface ICache + { + /// + /// Gets a cached item by key. + /// + /// Type of the cached item. + /// The cache key. + /// The cached item, or default if not found. + T? Get(string key); + + /// + /// Adds or updates an item in the cache. + /// + /// Type of the item. + /// The cache key. + /// The value to cache. + /// Optional absolute expiration time. + void Set(string key, T value, TimeSpan? absoluteExpiration = null); + + /// + /// Removes an item from the cache. + /// + /// The cache key. + void Remove(string key); + + /// + /// Checks if a key exists in the cache. + /// + /// The cache key. + /// True if the key exists, otherwise false. + bool Contains(string key); + + /// + /// Populates the cache using a custom technique defined by the implementing class. + /// + Task Populate(); + } +} \ No newline at end of file diff --git a/src/Common/Cache/ProblemCache.cs b/src/Common/Cache/ProblemCache.cs new file mode 100644 index 0000000..37c2211 --- /dev/null +++ b/src/Common/Cache/ProblemCache.cs @@ -0,0 +1,77 @@ + +using Common.Constants; +using Common.Models; +using Common.Repositories; +using Microsoft.Azure.Cosmos; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Common.Cache +{ + public class ProblemCache : BaseCache + { + private const int RefreshIntervalInHours = 3; + private readonly IConfiguration configuration; + private readonly IProblemRepository problemRepository; + ILogger logger; + private DateTime lastLoadedTime = DateTime.MinValue; + private Dictionary problemsCache = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public ProblemCache(IProblemRepository problemRepository, IConfiguration configuration, ILogger logger) + { + this.problemRepository = problemRepository; + this.configuration = configuration; + this.logger = logger; + Task.Run(() => this.StartBackgroundRefreshAsync(CancellationToken.None)); + } + + public async override Task Populate() + { + await LoadLatestDataAsync(); + // Ideally we should just make a call to redi here and return the data. + Set>(CacheConstants.ProblemCacheKey, problemsCache); + return; + } + + + private async Task LoadLatestDataAsync() + { + int maxRetries = 3; + for (int i = 0; i < maxRetries; i++) + { + try + { + var results = await problemRepository.GetAllProblemsAsync(); + lastLoadedTime = DateTime.UtcNow; + results = results.OrderBy(p => int.TryParse(p.id, out int id) ? id : -1).ToList(); + problemsCache = results.ToDictionary(p => p.id, StringComparer.OrdinalIgnoreCase); + this.logger.LogInformation($"Loaded {problemsCache.Count} problems from Cosmos DB at {lastLoadedTime}"); + break; + } + catch (Exception ex) + { + this.logger.LogError($"Error loading data from Cosmos DB. {ex}"); + await Task.Delay(TimeSpan.FromSeconds(2 * (i + 1))); + } + } + } + + private async Task StartBackgroundRefreshAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + try + { + await LoadLatestDataAsync(); + } + catch (Exception ex) + { + this.logger.LogError($"Error during background data refresh: {ex}"); + } + + await Task.Delay(TimeSpan.FromHours(RefreshIntervalInHours), cancellationToken); + } + } + + } +} diff --git a/src/Common/Common.csproj b/src/Common/Common.csproj index 206b89a..844bdc5 100644 --- a/src/Common/Common.csproj +++ b/src/Common/Common.csproj @@ -1,10 +1,18 @@  - Exe + Library net8.0 enable enable + + + + + + + + diff --git a/src/Common/Constants/CacheConstants.cs b/src/Common/Constants/CacheConstants.cs new file mode 100644 index 0000000..28f2b63 --- /dev/null +++ b/src/Common/Constants/CacheConstants.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Common.Constants +{ + public class CacheConstants + { + public const string ProblemCacheKey = "ProblemCache"; + } +} diff --git a/src/Common/Constants/ConfigurationConstants.cs b/src/Common/Constants/ConfigurationConstants.cs new file mode 100644 index 0000000..bb21455 --- /dev/null +++ b/src/Common/Constants/ConfigurationConstants.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Common.Constants +{ + public static class ConfigurationConstants + { + #region Application Settings + public const string CosmosDBUriKey = "ApplicationSettings:CosmosDbUri"; + public const string CosmosDBAccountKey = "ApplicationSettings:AccountKey"; + public const string ApplicationSettings = "ApplicationSettings"; + public const string LCProjectContainerNameKey = "LCProject:ContainerName"; + public const string LCProjectDatabaseNameKey = "LCProject:DatabaseName"; + #endregion + } +} diff --git a/src/Common/Enums/CosmosContainerEnum.cs b/src/Common/Enums/CosmosContainerEnum.cs new file mode 100644 index 0000000..538f048 --- /dev/null +++ b/src/Common/Enums/CosmosContainerEnum.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Common.Enums +{ + public enum CosmosContainerEnum + { + ProblemsContainer + } +} diff --git a/src/Common/Factories/CosmosContainerFactory.cs b/src/Common/Factories/CosmosContainerFactory.cs new file mode 100644 index 0000000..d526140 --- /dev/null +++ b/src/Common/Factories/CosmosContainerFactory.cs @@ -0,0 +1,48 @@ +using Common.Constants; +using Common.Enums; +using Common.Models.Miscellaneous; +using Microsoft.Azure.Cosmos; +using Microsoft.Extensions.Configuration; + +namespace Common.Factories +{ + public class CosmosContainerFactory : ICosmosContainerFactory + { + private CosmosClient _cosmosClient; + + private readonly IConfiguration _configuration; + + public CosmosContainerFactory(CosmosClient cosmosClient, IConfiguration configuration) + { + _cosmosClient = cosmosClient; + _configuration = configuration; + } + + public Container GetContainer(CosmosContainerEnum container) + { + var containerDetails = LoadContainerDetails(); + switch (container) + { + case CosmosContainerEnum.ProblemsContainer: + var dbId = containerDetails[container].DatabaseName; + var containerId = containerDetails[container].ContainerName; + var db = _cosmosClient.GetDatabase(dbId); + return db.GetContainer(containerId); + default: + throw new ArgumentOutOfRangeException(nameof(container), container, null); + } + } + + private Dictionary LoadContainerDetails() + { + var config = _configuration.GetSection(ConfigurationConstants.ApplicationSettings); + return new Dictionary + { + { + CosmosContainerEnum.ProblemsContainer, + new ContainerDetails(config[ConfigurationConstants.LCProjectDatabaseNameKey], config[ConfigurationConstants.LCProjectContainerNameKey]) + } + }; + } + } +} diff --git a/src/Common/Factories/ICosmosContainerFactory.cs b/src/Common/Factories/ICosmosContainerFactory.cs new file mode 100644 index 0000000..f177298 --- /dev/null +++ b/src/Common/Factories/ICosmosContainerFactory.cs @@ -0,0 +1,10 @@ +using Common.Enums; +using Microsoft.Azure.Cosmos; + +namespace Common.Factories +{ + public interface ICosmosContainerFactory + { + Container GetContainer(CosmosContainerEnum container); + } +} \ No newline at end of file diff --git a/src/Common/Models/Miscellaneous/ContainerDetails.cs b/src/Common/Models/Miscellaneous/ContainerDetails.cs new file mode 100644 index 0000000..9d3a630 --- /dev/null +++ b/src/Common/Models/Miscellaneous/ContainerDetails.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Common.Models.Miscellaneous +{ + public class ContainerDetails + { + public string DatabaseName { get; set; } + public string ContainerName { get; set; } + + public ContainerDetails(string databaseName, string containerName) + { + DatabaseName = databaseName; + ContainerName = containerName; + } + } +} diff --git a/src/Backend/Models/Public/QueryParam.cs b/src/Common/Models/QueryParam.cs similarity index 100% rename from src/Backend/Models/Public/QueryParam.cs rename to src/Common/Models/QueryParam.cs diff --git a/src/Common/Program.cs b/src/Common/Program.cs deleted file mode 100644 index 3095376..0000000 --- a/src/Common/Program.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Common; - -public class Program -{ - static void Main(string[] args) - { - Console.WriteLine("Hello, World!"); - } -} diff --git a/src/Common/Repositories/IProblemRepository.cs b/src/Common/Repositories/IProblemRepository.cs new file mode 100644 index 0000000..17abe1b --- /dev/null +++ b/src/Common/Repositories/IProblemRepository.cs @@ -0,0 +1,9 @@ +using Common.Models; + +namespace Common.Repositories +{ + public interface IProblemRepository + { + Task> GetAllProblemsAsync(); + } +} \ No newline at end of file diff --git a/src/Common/Repositories/ProblemRepository.cs b/src/Common/Repositories/ProblemRepository.cs new file mode 100644 index 0000000..8ca5d29 --- /dev/null +++ b/src/Common/Repositories/ProblemRepository.cs @@ -0,0 +1,37 @@ +using Common.Enums; +using Common.Factories; +using Common.Models; +using Microsoft.Azure.Cosmos; +using Microsoft.Extensions.Logging; + +namespace Common.Repositories +{ + public class ProblemRepository : IProblemRepository + { + private readonly Container _problemContainer; + private readonly ILogger _logger; + + public ProblemRepository(ICosmosContainerFactory cosmosContainerFactory, + ILogger logger) + { + _problemContainer = cosmosContainerFactory.GetContainer(CosmosContainerEnum.ProblemsContainer); + _logger = logger; + } + + public async Task> GetAllProblemsAsync() + { + var query = "SELECT * FROM c"; + var queryDefinition = new QueryDefinition(query); + var queryResultSetIterator = _problemContainer.GetItemQueryIterator(queryDefinition); + List results = new List(); + while (queryResultSetIterator.HasMoreResults) + { + var response = await queryResultSetIterator.ReadNextAsync(); + results.AddRange(response.Select(item => new Problem(item))); + } + results = results.OrderBy(p => int.TryParse(p.id, out int id) ? id : -1).ToList(); + _logger.LogInformation($"Retrieved {results.Count} problems from Cosmos DB."); + return results; + } + } +} diff --git a/src/Synchronizer/Synchronizer.csproj b/src/Synchronizer/Synchronizer.csproj index 72a2484..36a7b00 100644 --- a/src/Synchronizer/Synchronizer.csproj +++ b/src/Synchronizer/Synchronizer.csproj @@ -8,8 +8,8 @@ - - + +