Skip to content
Open
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
4 changes: 2 additions & 2 deletions 10.0/Apps/DeveloperBalance/AppShell.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public AppShell()
}
public static async Task DisplaySnackbarAsync(string message)
{
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5));

var snackbarOptions = new SnackbarOptions
{
Expand All @@ -41,7 +41,7 @@ public static async Task DisplayToastAsync(string message)

var toast = Toast.Make(message, textSize: 18);

var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await toast.Show(cts.Token);
}

Expand Down
41 changes: 29 additions & 12 deletions 10.0/Apps/DeveloperBalance/Data/CategoryRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ namespace DeveloperBalance.Data;
public class CategoryRepository
{
private bool _hasBeenInitialized = false;
private readonly SemaphoreSlim _initLock = new(1, 1);
private readonly ILogger _logger;

/// <summary>
Expand All @@ -29,11 +30,15 @@ private async Task Init()
if (_hasBeenInitialized)
return;

await using var connection = new SqliteConnection(Constants.DatabasePath);
await connection.OpenAsync();

await _initLock.WaitAsync();
try
{
if (_hasBeenInitialized)
return;

await using var connection = new SqliteConnection(Constants.DatabasePath);
await connection.OpenAsync();

var createTableCmd = connection.CreateCommand();
createTableCmd.CommandText = @"
CREATE TABLE IF NOT EXISTS Category (
Expand All @@ -42,14 +47,18 @@ CREATE TABLE IF NOT EXISTS Category (
Color TEXT NOT NULL
);";
await createTableCmd.ExecuteNonQueryAsync();

_hasBeenInitialized = true;
}
catch (Exception e)
{
_logger.LogError(e, "Error creating Category table");
throw;
}

_hasBeenInitialized = true;
finally
{
_initLock.Release();
}
}

/// <summary>
Expand Down Expand Up @@ -171,14 +180,22 @@ public async Task<int> DeleteItemAsync(Category item)
/// </summary>
public async Task DropTableAsync()
{
await Init();
await using var connection = new SqliteConnection(Constants.DatabasePath);
await connection.OpenAsync();
await _initLock.WaitAsync();
try
{
await using var connection = new SqliteConnection(Constants.DatabasePath);
await connection.OpenAsync();

var dropTableCmd = connection.CreateCommand();
dropTableCmd.CommandText = "DROP TABLE IF EXISTS Category";
var dropTableCmd = connection.CreateCommand();
dropTableCmd.CommandText = "DROP TABLE IF EXISTS Category";

await dropTableCmd.ExecuteNonQueryAsync();
_hasBeenInitialized = false;
await dropTableCmd.ExecuteNonQueryAsync();

_hasBeenInitialized = false;
}
finally
{
_initLock.Release();
}
}
}
2 changes: 2 additions & 0 deletions 10.0/Apps/DeveloperBalance/Data/JsonContext.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.Text.Json.Serialization;
using DeveloperBalance.Models;

namespace DeveloperBalance.Data;

[JsonSerializable(typeof(Project))]
[JsonSerializable(typeof(ProjectTask))]
[JsonSerializable(typeof(ProjectsJson))]
Expand Down
54 changes: 40 additions & 14 deletions 10.0/Apps/DeveloperBalance/Data/ProjectRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ namespace DeveloperBalance.Data;
public class ProjectRepository
{
private bool _hasBeenInitialized = false;
private readonly SemaphoreSlim _initLock = new(1, 1);
private readonly ILogger _logger;
private readonly TaskRepository _taskRepository;
private readonly TagRepository _tagRepository;
Expand All @@ -35,11 +36,15 @@ private async Task Init()
if (_hasBeenInitialized)
return;

await using var connection = new SqliteConnection(Constants.DatabasePath);
await connection.OpenAsync();

await _initLock.WaitAsync();
try
{
if (_hasBeenInitialized)
return;

await using var connection = new SqliteConnection(Constants.DatabasePath);
await connection.OpenAsync();

var createTableCmd = connection.CreateCommand();
createTableCmd.CommandText = @"
CREATE TABLE IF NOT EXISTS Project (
Expand All @@ -50,14 +55,18 @@ CREATE TABLE IF NOT EXISTS Project (
CategoryID INTEGER NOT NULL
);";
await createTableCmd.ExecuteNonQueryAsync();

_hasBeenInitialized = true;
}
catch (Exception e)
{
_logger.LogError(e, "Error creating Project table");
throw;
}

_hasBeenInitialized = true;
finally
{
_initLock.Release();
}
}

/// <summary>
Expand Down Expand Up @@ -87,10 +96,19 @@ public async Task<List<Project>> ListAsync()
});
}

// Fetch all tasks and all tags in one query each, then assign in memory.
// This reduces 2N+1 queries to 3 regardless of project count.
var allTasks = await _taskRepository.ListAsync();
var allTagsByProject = await _tagRepository.ListAllByProjectAsync();

var tasksByProject = allTasks
.GroupBy(t => t.ProjectID)
.ToDictionary(g => g.Key, g => g.ToList());

foreach (var project in projects)
{
project.Tags = await _tagRepository.ListAsync(project.ID);
project.Tasks = await _taskRepository.ListAsync(project.ID);
project.Tasks = tasksByProject.TryGetValue(project.ID, out var tasks) ? tasks : [];
project.Tags = allTagsByProject.TryGetValue(project.ID, out var tags) ? tags : [];
}

return projects;
Expand Down Expand Up @@ -197,16 +215,24 @@ public async Task<int> DeleteItemAsync(Project item)
/// </summary>
public async Task DropTableAsync()
{
await Init();
await using var connection = new SqliteConnection(Constants.DatabasePath);
await connection.OpenAsync();
await _initLock.WaitAsync();
try
{
await using var connection = new SqliteConnection(Constants.DatabasePath);
await connection.OpenAsync();

var dropCmd = connection.CreateCommand();
dropCmd.CommandText = "DROP TABLE IF EXISTS Project";
await dropCmd.ExecuteNonQueryAsync();

var dropCmd = connection.CreateCommand();
dropCmd.CommandText = "DROP TABLE IF EXISTS Project";
await dropCmd.ExecuteNonQueryAsync();
_hasBeenInitialized = false;
}
finally
{
_initLock.Release();
}

await _taskRepository.DropTableAsync();
await _tagRepository.DropTableAsync();
_hasBeenInitialized = false;
}
}
18 changes: 4 additions & 14 deletions 10.0/Apps/DeveloperBalance/Data/SeedDataService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public SeedDataService(ProjectRepository projectRepository, TaskRepository taskR

public async Task LoadSeedDataAsync()
{
ClearTables();
await ClearTables();

await using Stream templateStream = await FileSystem.OpenAppPackageFileAsync(_seedDataFilePath);

Expand Down Expand Up @@ -83,19 +83,9 @@ public async Task LoadSeedDataAsync()
}
}

private async void ClearTables()
private async Task ClearTables()
{
try
{
await Task.WhenAll(
_projectRepository.DropTableAsync(),
_taskRepository.DropTableAsync(),
_tagRepository.DropTableAsync(),
_categoryRepository.DropTableAsync());
}
catch (Exception e)
{
Console.WriteLine(e);
}
await _projectRepository.DropTableAsync();
await _categoryRepository.DropTableAsync();
}
}
86 changes: 71 additions & 15 deletions 10.0/Apps/DeveloperBalance/Data/TagRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ namespace DeveloperBalance.Data;
public class TagRepository
{
private bool _hasBeenInitialized = false;
private readonly SemaphoreSlim _initLock = new(1, 1);
private readonly ILogger _logger;

/// <summary>
Expand All @@ -29,11 +30,15 @@ private async Task Init()
if (_hasBeenInitialized)
return;

await using var connection = new SqliteConnection(Constants.DatabasePath);
await connection.OpenAsync();

await _initLock.WaitAsync();
try
{
if (_hasBeenInitialized)
return;

await using var connection = new SqliteConnection(Constants.DatabasePath);
await connection.OpenAsync();

var createTableCmd = connection.CreateCommand();
createTableCmd.CommandText = @"
CREATE TABLE IF NOT EXISTS Tag (
Expand All @@ -50,14 +55,18 @@ CREATE TABLE IF NOT EXISTS ProjectsTags (
PRIMARY KEY(ProjectID, TagID)
);";
await createTableCmd.ExecuteNonQueryAsync();

_hasBeenInitialized = true;
}
catch (Exception e)
{
_logger.LogError(e, "Error creating tables");
throw;
}

_hasBeenInitialized = true;
finally
{
_initLock.Release();
}
}

/// <summary>
Expand Down Expand Up @@ -88,6 +97,46 @@ public async Task<List<Tag>> ListAsync()
return tags;
}

/// <summary>
/// Retrieves all tags grouped by their associated project ID in a single query.
/// </summary>
/// <returns>A dictionary mapping project ID to its list of <see cref="Tag"/> objects.</returns>
public async Task<Dictionary<int, List<Tag>>> ListAllByProjectAsync()
{
await Init();
await using var connection = new SqliteConnection(Constants.DatabasePath);
await connection.OpenAsync();

var selectCmd = connection.CreateCommand();
selectCmd.CommandText = @"
SELECT t.ID, t.Title, t.Color, pt.ProjectID
FROM Tag t
JOIN ProjectsTags pt ON t.ID = pt.TagID";

var result = new Dictionary<int, List<Tag>>();

await using var reader = await selectCmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
var tag = new Tag
{
ID = reader.GetInt32(0),
Title = reader.GetString(1),
Color = reader.GetString(2)
};
var projectID = reader.GetInt32(3);

if (!result.TryGetValue(projectID, out var list))
{
list = [];
result[projectID] = list;
}
list.Add(tag);
}

return result;
}

/// <summary>
/// Retrieves a list of tags associated with a specific project.
/// </summary>
Expand All @@ -105,7 +154,7 @@ SELECT t.*
FROM Tag t
JOIN ProjectsTags pt ON t.ID = pt.TagID
WHERE pt.ProjectID = @ProjectID";
selectCmd.Parameters.AddWithValue("ProjectID", projectID);
selectCmd.Parameters.AddWithValue("@ProjectID", projectID);

var tags = new List<Tag>();

Expand Down Expand Up @@ -288,17 +337,24 @@ public async Task<int> DeleteItemAsync(Tag item, int projectID)
/// </summary>
public async Task DropTableAsync()
{
await Init();
await using var connection = new SqliteConnection(Constants.DatabasePath);
await connection.OpenAsync();
await _initLock.WaitAsync();
try
{
await using var connection = new SqliteConnection(Constants.DatabasePath);
await connection.OpenAsync();

var dropTableCmd = connection.CreateCommand();
dropTableCmd.CommandText = "DROP TABLE IF EXISTS Tag";
await dropTableCmd.ExecuteNonQueryAsync();
var dropTableCmd = connection.CreateCommand();
dropTableCmd.CommandText = "DROP TABLE IF EXISTS Tag";
await dropTableCmd.ExecuteNonQueryAsync();

dropTableCmd.CommandText = "DROP TABLE IF EXISTS ProjectsTags";
await dropTableCmd.ExecuteNonQueryAsync();
dropTableCmd.CommandText = "DROP TABLE IF EXISTS ProjectsTags";
await dropTableCmd.ExecuteNonQueryAsync();

_hasBeenInitialized = false;
_hasBeenInitialized = false;
}
finally
{
_initLock.Release();
}
}
}
Loading
Loading