Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -197,4 +197,5 @@ dependency-reduced-pom.xml
README.html
*.iml
.idea
.exercism
.exercism
/src/.vs
19 changes: 0 additions & 19 deletions src/Backend/AppContext.cs

This file was deleted.

10 changes: 7 additions & 3 deletions src/Backend/Backend.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
Expand All @@ -8,14 +8,18 @@

<ItemGroup>
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.23.0" />
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.53.1" />
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.54.0" />
<PackageReference Include="Microsoft.Extensions.Logging.ApplicationInsights" Version="2.23.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>

<ItemGroup>
<Folder Include="Docs\" />
</ItemGroup>

</Project>
6 changes: 6 additions & 0 deletions src/Backend/Backend.csproj.user
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ActiveDebugProfile>https</ActiveDebugProfile>
</PropertyGroup>
</Project>
21 changes: 12 additions & 9 deletions src/Backend/Controllers/ProblemsController.cs
Original file line number Diff line number Diff line change
@@ -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<ProblemsController> logger;
private readonly IConfiguration configuration;
public ProblemsController(AppContext appContext, ILogger<ProblemsController> logger, IConfiguration configuration)
private readonly DataProvider dataProvider;
public ProblemsController(ILogger<ProblemsController> logger,
DataProvider dataProvider,
IConfiguration configuration)
{
this.appContext = appContext;
this.logger = logger;
this.dataProvider = dataProvider;
this.configuration = configuration;
}

Expand All @@ -28,23 +31,23 @@ public ActionResult<string> GetHome()

[HttpGet]
[Route("problems")]
public async Task<ActionResult<IEnumerable<ProblemPublicModel>>> GetProblems(
public async Task<ActionResult<IEnumerable<Problem>>> GetProblems(
[FromQuery(Name = QueryParam.Skip)] int skip = 0,
[FromQuery(Name = QueryParam.Limit)] int limit = 50,
[FromQuery(Name = QueryParam.Company)] List<string>? companies = null,
[FromQuery(Name = QueryParam.Difficulty)] List<Common.Models.Difficulty>? difficulties = null,
[FromQuery(Name = QueryParam.Difficulty)] List<Difficulty>? difficulties = null,
[FromQuery(Name = QueryParam.Tag)] List<string>? 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<ActionResult<ProblemPublicModel>> GetProblems(string id)
public async Task<ActionResult<Problem>> GetProblems(string id)
{
var problem = await appContext.dataProvider.GetProblemByIdAsync(id);
var problem = await dataProvider.GetProblemByIdAsync(id);
if (problem != null)
{
return Ok(problem);
Expand Down
8 changes: 8 additions & 0 deletions src/Backend/Docs/NextPlanOfAction.md
Original file line number Diff line number Diff line change
@@ -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**
9 changes: 9 additions & 0 deletions src/Backend/Filters/IFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Common.Models;

namespace Backend.Filters
{
public interface IFilter
{
public List<Problem> ApplyFilterAsync(List<Problem> problems);
}
}
Original file line number Diff line number Diff line change
@@ -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<string> companies;
private List<Common.Models.Difficulty> difficulties;
private List<Difficulty> difficulties;
private List<string> tags;

public ProblemFilter(int skip, int limit, List<string>? companies, List<Common.Models.Difficulty>? difficulties, List<string>? tags)
public ProblemFilter(int skip, int limit, List<string>? companies, List<Difficulty>? difficulties, List<string>? tags)
{
this.skip = skip;
this.limit = Math.Min(limit, 50);
this.companies = companies ?? new List<string>();
this.difficulties = difficulties ?? new List<Common.Models.Difficulty>();
this.difficulties = difficulties ?? new List<Difficulty>();
this.tags = tags ?? new List<string>();
}

public List<Common.Models.Problem> ApplyFilterAsync(List<Common.Models.Problem> problems)
public List<Problem> ApplyFilterAsync(List<Problem> problems)
{
List<Common.Models.Problem> filteredProblems = problems;
List<Problem> 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();
Expand Down
Empty file.
1 change: 0 additions & 1 deletion src/Backend/Models/Public/Problem.cs

This file was deleted.

88 changes: 15 additions & 73 deletions src/Backend/Operations/DataProvider.cs
Original file line number Diff line number Diff line change
@@ -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<DataProvider> logger;
private DateTime lastLoadedTime = DateTime.MinValue;
private Task backgroundRefreshTask;
private Dictionary<string, Problem> problemsCache = new Dictionary<string, Problem>(StringComparer.OrdinalIgnoreCase);

public DataProvider(CosmosClient client, IConfiguration configuration, ILogger<DataProvider> logger)
private ICache _problemCache;
private ILogger<DataProvider> _logger;
public DataProvider([FromKeyedServices(CacheConstants.ProblemCacheKey)] ICache problemCache, ILogger<DataProvider> 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<List<Problem>> GetProblemsAsync(IFilter? filter = null)
Expand All @@ -43,69 +37,17 @@ public async Task<List<Problem>> GetProblemsAsync(IFilter? filter = null)

private async Task<Dictionary<string, Problem>> 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<string>("ApplicationSettings:CosmosDbDatabaseId");
var containerId = configuration.GetValue<string>("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<ProblemSchema>(queryDefinition);

List<Problem> results = new List<Problem>();
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<Dictionary<string, Problem>>(CacheConstants.ProblemCacheKey) ?? new Dictionary<string, Problem>(StringComparer.OrdinalIgnoreCase);
}
}
}
7 changes: 0 additions & 7 deletions src/Backend/Operations/IFilter.cs

This file was deleted.

42 changes: 33 additions & 9 deletions src/Backend/Program.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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<ILogger<DataProvider>>()
)
);

#region Register Cosmos related services
services.AddSingleton<CosmosClient>(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<ICosmosContainerFactory, CosmosContainerFactory>();
#endregion

#region Register Cache
services.AddKeyedSingleton<ICache, ProblemCache>(CacheConstants.ProblemCacheKey);
#endregion

#region Register Repositories
services.AddTransient<IProblemRepository, ProblemRepository>();
#endregion

#region Register Miscellaneous Services
services.AddTransient<DataProvider>();
#endregion

var app = builder.Build();
ILogger logger = app.Logger;
Expand Down
6 changes: 3 additions & 3 deletions src/Backend/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Loading