Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion src/AppHost/AppHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@

// Exclude the compiler-generated Program class from coverage.
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage(Justification = "Aspire host bootstrap — not business logic")]
public partial class Program { }
internal partial class Program { }
10 changes: 10 additions & 0 deletions src/AppHost/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//=======================================================
//Copyright (c) 2026. All rights reserved.
//File Name : AssemblyInfo.cs
//Company : mpaulosky
//Author : Matthew Paulosky
//Solution Name : MyBlog
//Project Name : AppHost
//=======================================================

[assembly: CLSCompliant(false)]
38 changes: 26 additions & 12 deletions src/Web/Components/Theme/ThemeProvider.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
//Project Name : Web
//=======================================================

using System.Diagnostics.CodeAnalysis;

using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;

namespace MyBlog.Web.Components.Theme;

[SuppressMessage("Design", "CA1515:Consider making public types internal", Justification = "Razor component discovery and bUnit rendering require the component type to remain public.")]
public partial class ThemeProvider : ComponentBase
{
[Inject] private IJSRuntime Js { get; set; } = default!;
Expand All @@ -28,32 +31,43 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
return;
}

CurrentColor = await GetStoredThemeValueAsync("themeManager.getColor", CurrentColor);
CurrentBrightness = await GetStoredThemeValueAsync("themeManager.getBrightness", CurrentBrightness);
var currentColor = await GetStoredThemeValueAsync("themeManager.getColor", CurrentColor).ConfigureAwait(false);
var currentBrightness = await GetStoredThemeValueAsync("themeManager.getBrightness", CurrentBrightness).ConfigureAwait(false);

await TryMarkInitializedAsync();
await InvokeAsync(StateHasChanged);
await TryMarkInitializedAsync().ConfigureAwait(false);
await InvokeAsync(() =>
{
CurrentColor = currentColor;
CurrentBrightness = currentBrightness;
StateHasChanged();
}).ConfigureAwait(false);
}

public async Task SetColor(string color)
{
CurrentColor = color;
StateHasChanged();
await Js.InvokeVoidAsync("themeManager.setColor", color);
await Js.InvokeVoidAsync("themeManager.setColor", color).ConfigureAwait(false);
await InvokeAsync(() =>
{
CurrentColor = color;
StateHasChanged();
}).ConfigureAwait(false);
}

public async Task SetBrightness(string brightness)
{
CurrentBrightness = brightness;
StateHasChanged();
await Js.InvokeVoidAsync("themeManager.setBrightness", brightness);
await Js.InvokeVoidAsync("themeManager.setBrightness", brightness).ConfigureAwait(false);
await InvokeAsync(() =>
{
CurrentBrightness = brightness;
StateHasChanged();
}).ConfigureAwait(false);
}

private async Task<string> GetStoredThemeValueAsync(string identifier, string fallback)
{
try
{
return await Js.InvokeAsync<string>(identifier);
return await Js.InvokeAsync<string>(identifier).ConfigureAwait(false);
}
catch (JSException)
{
Expand All @@ -71,7 +85,7 @@ private async Task TryMarkInitializedAsync()
{
try
{
await Js.InvokeVoidAsync("themeManager.markInitialized");
await Js.InvokeVoidAsync("themeManager.markInitialized").ConfigureAwait(false);
}
catch (JSException)
{
Expand Down
60 changes: 39 additions & 21 deletions src/Web/Data/MongoDbBlogPostRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,52 +14,70 @@ internal sealed class MongoDbBlogPostRepository(IDbContextFactory<BlogDbContext>
{
public async Task<BlogPost?> GetByIdAsync(ObjectId id, CancellationToken ct = default)
{
await using var ctx = await contextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
return await ctx.BlogPosts.AsNoTracking()
var ctx = await contextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
await using (ctx.ConfigureAwait(false))
{
return await ctx.BlogPosts.AsNoTracking()
.FirstOrDefaultAsync(p => p.Id == id, ct).ConfigureAwait(false);
}
}

public async Task<IReadOnlyList<BlogPost>> GetAllAsync(CancellationToken ct = default)
{
await using var ctx = await contextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
return await ctx.BlogPosts.AsNoTracking()
var ctx = await contextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
await using (ctx.ConfigureAwait(false))
{
return await ctx.BlogPosts.AsNoTracking()
.OrderByDescending(p => p.CreatedAt)
.ToListAsync(ct).ConfigureAwait(false);
}
}

public async Task<bool> ExistsByCategoryAsync(ObjectId categoryId, CancellationToken ct = default)
{
await using var ctx = await contextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
return await ctx.BlogPosts.AsNoTracking()
var ctx = await contextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
await using (ctx.ConfigureAwait(false))
{
return await ctx.BlogPosts.AsNoTracking()
.AnyAsync(p => p.CategoryId == categoryId, ct).ConfigureAwait(false);
}
}

public async Task AddAsync(BlogPost post, CancellationToken ct = default)
{
await using var ctx = await contextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
await ctx.BlogPosts.AddAsync(post, ct).ConfigureAwait(false);
await ctx.SaveChangesAsync(ct).ConfigureAwait(false);
var ctx = await contextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
await using (ctx.ConfigureAwait(false))
{
await ctx.BlogPosts.AddAsync(post, ct).ConfigureAwait(false);
await ctx.SaveChangesAsync(ct).ConfigureAwait(false);
}
}

public async Task UpdateAsync(BlogPost post, CancellationToken ct = default)
{
await using var ctx = await contextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
var entry = ctx.Attach(post);
// Version was incremented by post.Update(); the original value in the DB is Version - 1.
// EF Core uses OriginalValue in the WHERE filter to detect concurrent modifications.
entry.Property(p => p.Version).OriginalValue = post.Version - 1;
entry.State = EntityState.Modified;
await ctx.SaveChangesAsync(ct).ConfigureAwait(false);
var ctx = await contextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
await using (ctx.ConfigureAwait(false))
{
var entry = ctx.Attach(post);
// Version was incremented by post.Update(); the original value in the DB is Version - 1.
// EF Core uses OriginalValue in the WHERE filter to detect concurrent modifications.
entry.Property(p => p.Version).OriginalValue = post.Version - 1;
entry.State = EntityState.Modified;
await ctx.SaveChangesAsync(ct).ConfigureAwait(false);
}
}

public async Task DeleteAsync(ObjectId id, CancellationToken ct = default)
{
await using var ctx = await contextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
var post = await ctx.BlogPosts.FindAsync([id], ct).ConfigureAwait(false);
if (post is not null)
var ctx = await contextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
await using (ctx.ConfigureAwait(false))
{
ctx.BlogPosts.Remove(post);
await ctx.SaveChangesAsync(ct).ConfigureAwait(false);
var post = await ctx.BlogPosts.FindAsync([id], ct).ConfigureAwait(false);
if (post is not null)
{
ctx.BlogPosts.Remove(post);
await ctx.SaveChangesAsync(ct).ConfigureAwait(false);
}
}
}
}
73 changes: 47 additions & 26 deletions src/Web/Data/MongoDbCategoryRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,57 +14,78 @@ internal sealed class MongoDbCategoryRepository(IDbContextFactory<BlogDbContext>
{
public async Task<Category?> GetByIdAsync(ObjectId id, CancellationToken ct = default)
{
await using var ctx = await contextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
return await ctx.Categories.AsNoTracking()
.FirstOrDefaultAsync(c => c.Id == id, ct).ConfigureAwait(false);
var ctx = await contextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
await using (ctx.ConfigureAwait(false))
{
return await ctx.Categories.AsNoTracking()
.FirstOrDefaultAsync(c => c.Id == id, ct).ConfigureAwait(false);
}
}

public async Task<IReadOnlyList<Category>> GetAllAsync(CancellationToken ct = default)
{
await using var ctx = await contextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
return await ctx.Categories.AsNoTracking()
.OrderBy(c => c.Name)
.ToListAsync(ct).ConfigureAwait(false);
var ctx = await contextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
await using (ctx.ConfigureAwait(false))
{
return await ctx.Categories.AsNoTracking()
.OrderBy(c => c.Name)
.ToListAsync(ct).ConfigureAwait(false);
}
}

public async Task<bool> ExistsByNameAsync(string name, CancellationToken ct = default)
{
await using var ctx = await contextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
var normalizedName = name.Trim();
return await ctx.Categories.AsNoTracking()
.AnyAsync(c => c.Name == normalizedName, ct).ConfigureAwait(false);
var ctx = await contextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
await using (ctx.ConfigureAwait(false))
{
var normalizedName = name.Trim();
return await ctx.Categories.AsNoTracking()
.AnyAsync(c => c.Name == normalizedName, ct).ConfigureAwait(false);
}
}

public async Task<bool> ExistsByNameExcludingAsync(string name, ObjectId excludedId, CancellationToken ct = default)
{
await using var ctx = await contextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
var normalizedName = name.Trim();
return await ctx.Categories.AsNoTracking()
.AnyAsync(c => c.Name == normalizedName && c.Id != excludedId, ct).ConfigureAwait(false);
var ctx = await contextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
await using (ctx.ConfigureAwait(false))
{
var normalizedName = name.Trim();
return await ctx.Categories.AsNoTracking()
.AnyAsync(c => c.Name == normalizedName && c.Id != excludedId, ct).ConfigureAwait(false);
}
}

public async Task AddAsync(Category category, CancellationToken ct = default)
{
await using var ctx = await contextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
await ctx.Categories.AddAsync(category, ct).ConfigureAwait(false);
await ctx.SaveChangesAsync(ct).ConfigureAwait(false);
var ctx = await contextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
await using (ctx.ConfigureAwait(false))
{
await ctx.Categories.AddAsync(category, ct).ConfigureAwait(false);
await ctx.SaveChangesAsync(ct).ConfigureAwait(false);
}
}

public async Task UpdateAsync(Category category, CancellationToken ct = default)
{
await using var ctx = await contextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
ctx.Categories.Update(category);
await ctx.SaveChangesAsync(ct).ConfigureAwait(false);
var ctx = await contextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
await using (ctx.ConfigureAwait(false))
{
ctx.Categories.Update(category);
await ctx.SaveChangesAsync(ct).ConfigureAwait(false);
}
}

public async Task DeleteAsync(ObjectId id, CancellationToken ct = default)
{
await using var ctx = await contextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
var category = await ctx.Categories.FindAsync([id], ct).ConfigureAwait(false);
if (category is not null)
var ctx = await contextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
await using (ctx.ConfigureAwait(false))
{
ctx.Categories.Remove(category);
await ctx.SaveChangesAsync(ct).ConfigureAwait(false);
var category = await ctx.Categories.FindAsync([id], ct).ConfigureAwait(false);
if (category is not null)
{
ctx.Categories.Remove(category);
await ctx.SaveChangesAsync(ct).ConfigureAwait(false);
}
}
}
}
15 changes: 11 additions & 4 deletions src/Web/Infrastructure/FileStorage/LocalDiskFileStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,17 @@ public async Task<string> AddFileAsync(FileData file)
var uniqueName = $"{Guid.NewGuid()}{extension}";
var filePath = Path.Combine(uploadsPath, uniqueName);

await using var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write);
await file.Content.CopyToAsync(fs).ConfigureAwait(false);
var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write);
try
{
await file.Content.CopyToAsync(fs).ConfigureAwait(false);

LogFileSaved(_logger, uniqueName);
return uniqueName;
LogFileSaved(_logger, uniqueName);
return uniqueName;
}
finally
{
await fs.DisposeAsync().ConfigureAwait(false);
}
}
}
2 changes: 1 addition & 1 deletion src/Web/Security/RoleClaimsHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public static IReadOnlyList<string> ExpandRoleValues(string? claimValue)

var trimmed = claimValue.Trim();

if (trimmed.StartsWith("[", StringComparison.Ordinal))
if (trimmed.StartsWith('['))
{
try
{
Expand Down
28 changes: 18 additions & 10 deletions tests/AppHost.Tests/EnvVarTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

using FluentAssertions;

using Microsoft.Extensions.Logging.Abstractions;

namespace AppHost.Tests;

/// <summary>
Expand All @@ -35,13 +37,16 @@ public async Task WebResourceHasMongoDbConnectionString()
.Single(static r => r.Name == "web");

// Act
#pragma warning disable CS0618 // Type or member is obsolete
var envVars = await webResource.GetEnvironmentVariableValuesAsync(
DistributedApplicationOperation.Publish);
#pragma warning restore CS0618 // Type or member is obsolete
var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Publish);
var resolvedConfig = await ExecutionConfigurationBuilder
.Create(webResource)
.WithEnvironmentVariablesConfig()
.BuildAsync(executionContext, NullLogger.Instance, TestContext.Current.CancellationToken);

// Assert
envVars.Should().ContainKey("ConnectionStrings__myblog");
resolvedConfig.EnvironmentVariables
.Select(kvp => kvp.Key)
.Should().Contain("ConnectionStrings__myblog");
}

[Fact]
Expand All @@ -61,12 +66,15 @@ public async Task WebResourceHasRedisConnectionString()
.Single(static r => r.Name == "web");

// Act
#pragma warning disable CS0618 // Type or member is obsolete
var envVars = await webResource.GetEnvironmentVariableValuesAsync(
DistributedApplicationOperation.Publish);
#pragma warning restore CS0618 // Type or member is obsolete
var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Publish);
var resolvedConfig = await ExecutionConfigurationBuilder
.Create(webResource)
.WithEnvironmentVariablesConfig()
.BuildAsync(executionContext, NullLogger.Instance, TestContext.Current.CancellationToken);

// Assert
envVars.Should().ContainKey("ConnectionStrings__redis");
resolvedConfig.EnvironmentVariables
.Select(kvp => kvp.Key)
.Should().Contain("ConnectionStrings__redis");
}
}
Loading
Loading