Compare commits
9
Commits
0b5ebc3bb1
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46ff22b250 | ||
|
|
b6843919d8 | ||
|
|
eaa5f4a965 | ||
|
|
2990c82181 | ||
|
|
e5d7a2b73a | ||
|
|
0300402667 | ||
|
|
81cc1e4f64 | ||
|
|
4a7c8b31c5 | ||
|
|
49fe692ec7 |
No files matched your search
@@ -3,7 +3,7 @@
|
||||
"title": "Building a Chunked, Resumable File Upload System in .NET and Vanilla JS",
|
||||
"lead": "How to bypass reverse proxy payload limits, eliminate double-storage disk overhead, and build a resumable file uploader.",
|
||||
"isPublished": true,
|
||||
n"publishedAt": "2026-09-03",
|
||||
"publishedAt": "2026-09-03",
|
||||
"openGraphImage": "posts/chunked-resumable-file-uploads-dotnet/og-image.png"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
```json meta
|
||||
{
|
||||
"title": "\"Make it secure\"",
|
||||
"lead": "At KCDC, a speaker shared their team's security strategy for AI-generated hardware and access controls: simply prompting the model to 'make it secure.' A reflection on where we are with AI tooling and the vanishing bar for verifiable verification.",
|
||||
"isPublished": true,
|
||||
"publishedAt": "2026-09-11",
|
||||
"openGraphImage": "posts/make-it-secure/og-image.png"
|
||||
}
|
||||
```
|
||||
|
||||
I sat in on a talk yesterday at the Kansas City Developer Conference that was mostly about how a local small business owner was able to leverage AI tooling to advance his business outcomes.
|
||||
|
||||
One of the things he did was incorporate hardware into his business model, including facility access and monitoring-related devices. They developed both the hardware and software using agents and off-the-shelf components.
|
||||
|
||||
An audience member asked if he had any concerns about the security of what they were developing and deploying. The speaker's answer left me a bit stunned. I expected at least a qualified answer, or perhaps that they had red-teamed their work as part of the development process.
|
||||
|
||||
Instead, the real answer was that they just prompt the LLM to “make it secure” and run several reviews of the implementation across a couple of different models. Surprisingly, there wasn’t much reaction from the audience. I guess this is where we are now.
|
||||
|
||||
Honestly, I don’t really have any shade to throw at the presenter. It’s his company, and if he’s comfortable with the risk, more power to him. But I suppose I imagined more people would feel the way I do: wanting something verifiable and provable when it comes to assuring the quality, performance, and security of the platforms and products they build, with or without AI.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 158 KiB |
@@ -0,0 +1,19 @@
|
||||
```json meta
|
||||
{
|
||||
"title": "Thoughts On Typing Code Being Over",
|
||||
"lead": "Reflecting on the impact of AI code generation on junior developers, the value of grinding through problems, and the future of software engineering careers.",
|
||||
"isPublished": true,
|
||||
"publishedAt": "2026-09-06",
|
||||
"openGraphImage": "posts/thoughts-on-typing-code-being-over/og-image.png"
|
||||
}
|
||||
```
|
||||
|
||||
I have been ruminating on [this post](https://x.com/davidfowl/status/2095602923502010398) since first reading it. I understand it, and I can see the truth in it given how quickly you can generate code with an LLM. I won't lie: I still genuinely enjoy the experience of writing code by hand.
|
||||
|
||||
However, what gives me greater pause is the feeling that, to some extent, the tradeoff for an increasing reliance on LLMs is a decrease in my own knowledge and competence. So many of the people who inspire me, including David, didn't acquire their abilities by using AI. They spent two decades or more grinding away at problems. I am worried that in this new world, I will end up being robbed of those same development opportunities.
|
||||
|
||||
Maybe the truth is that these skills and knowledge won't matter moving forward. I don't quite see evidence of that yet; it seems like the people getting the most out of the LLM era are exactly these veteran folks. But if those skills eventually cease to matter, it raises the question: why waste any more time on this? Why not reinvest in some other field or career? That feels fatalistic, but it is also a reasonable path forward for someone like me who has at least another 19 years of mandatory work ahead.
|
||||
|
||||
I honestly don't know what the right answer is. I do think the conversation around this topic always needs to be bifurcated between what you do for fun and enjoyment, and what you do to put food on the table. The former is completely up to you, and all choices can be valid. The latter requires more thought and consideration, especially if you have other human beings relying on you for their economic well-being. You have to take into account the most important questions for a successful career: What am I good at? What do I want to spend my workdays doing? Does the world need that work? Will people pay me for that work?
|
||||
|
||||
I know it might seem silly to leap from "typing code is over" to "maybe I should be looking for a different career field." However, it feels like a valid thing to ponder, especially for someone who is only six years into this profession. We are facing a rapidly advancing technology that the most experienced people in our field are enamored with, and it appears to be reducing the need for developers at my level. At the very least, it is reducing the leverage that this knowledge and skill provide in earning a living.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
@@ -2,62 +2,28 @@ namespace Blog.Tests.EndToEnd;
|
||||
|
||||
public class BlogHostFactory<TProgram> : WebApplicationFactory<TProgram> where TProgram : class
|
||||
{
|
||||
private IHost? _host;
|
||||
|
||||
private void EnsureServer()
|
||||
public BlogHostFactory()
|
||||
{
|
||||
if (_host is null)
|
||||
{
|
||||
using var _ = CreateDefaultClient();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
_host?.Dispose();
|
||||
UseKestrel(server => server.Listen(IPAddress.Loopback, 0));
|
||||
}
|
||||
|
||||
public string ServerAddress
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureServer();
|
||||
StartServer();
|
||||
return ClientOptions.BaseAddress.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
protected override IHost CreateHost(IHostBuilder builder)
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
var testHost = builder
|
||||
.ConfigureWebHost(webHostBuilder =>
|
||||
{
|
||||
webHostBuilder.ConfigureLogging(config => config.ClearProviders());
|
||||
webHostBuilder.ConfigureTestServices(services =>
|
||||
{
|
||||
var postsDirectory = Path.Combine(Directory.GetCurrentDirectory(), "TestPosts");
|
||||
var postServiceOptions = new FilePostServiceOptions() { PostsDirectory = postsDirectory };
|
||||
services.AddSingleton(Options.Create(postServiceOptions));
|
||||
});
|
||||
})
|
||||
.Build();
|
||||
|
||||
builder.ConfigureWebHost(
|
||||
webHostBuilder => webHostBuilder.UseKestrel(
|
||||
o => o.Listen(IPAddress.Loopback, 0)
|
||||
)
|
||||
);
|
||||
|
||||
_host = builder.Build();
|
||||
_host.Start();
|
||||
|
||||
var server = _host.Services.GetRequiredService<IServer>();
|
||||
var addresses = server.Features.GetRequiredFeature<IServerAddressesFeature>();
|
||||
|
||||
ClientOptions.BaseAddress = addresses.Addresses
|
||||
.Select(x => new Uri(x))
|
||||
.Last();
|
||||
|
||||
testHost.Start();
|
||||
return testHost;
|
||||
builder.ConfigureLogging(config => config.ClearProviders());
|
||||
builder.ConfigureTestServices(services =>
|
||||
{
|
||||
var postsDirectory = Path.Combine(Directory.GetCurrentDirectory(), "TestPosts");
|
||||
var postServiceOptions = new FilePostServiceOptions() { PostsDirectory = postsDirectory };
|
||||
services.AddSingleton(Options.Create(postServiceOptions));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,12 @@ namespace Blog.Tests.EndToEnd;
|
||||
[TestFixture]
|
||||
public abstract class BlogTest : PageTest
|
||||
{
|
||||
private readonly BlogHostFactory<Program> _factory = new();
|
||||
private static readonly BlogHostFactory<Program> Factory = new();
|
||||
|
||||
public override BrowserNewContextOptions ContextOptions()
|
||||
{
|
||||
var options = base.ContextOptions();
|
||||
options.BaseURL = _factory.ServerAddress;
|
||||
options.BaseURL = Factory.ServerAddress;
|
||||
return options;
|
||||
}
|
||||
|
||||
@@ -30,10 +30,10 @@ public abstract class BlogTest : PageTest
|
||||
await Context.Tracing.StopAsync(new()
|
||||
{
|
||||
Path = Path.Combine(
|
||||
TestContext.CurrentContext.WorkDirectory,
|
||||
"playwright-traces",
|
||||
$"{TestContext.CurrentContext.Test.ClassName}.{TestContext.CurrentContext.Test.Name}.zip"
|
||||
)
|
||||
TestContext.CurrentContext.WorkDirectory,
|
||||
"playwright-traces",
|
||||
$"{TestContext.CurrentContext.Test.ClassName}.{TestContext.CurrentContext.Test.Name}.zip"
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
namespace Blog.Tests.EndToEnd;
|
||||
|
||||
public class CompositeHost(IHost testHost, IHost kestrelHost) : IHost
|
||||
{
|
||||
private readonly IHost _testHost = testHost;
|
||||
private readonly IHost _kestrelHost = kestrelHost;
|
||||
|
||||
public IServiceProvider Services => _testHost.Services;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_testHost.Dispose();
|
||||
_kestrelHost.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _testHost.StartAsync(cancellationToken);
|
||||
await _kestrelHost.StartAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _testHost.StopAsync(cancellationToken);
|
||||
await _kestrelHost.StopAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
using System.Text.Json;
|
||||
|
||||
using Markdig;
|
||||
using Markdig.Syntax;
|
||||
|
||||
namespace Blog.Tests.Unit;
|
||||
|
||||
[TestFixture]
|
||||
public class PostMetadataTests
|
||||
{
|
||||
private static readonly MarkdownPipeline MarkdownPipeline = new MarkdownPipelineBuilder()
|
||||
.UseAdvancedExtensions()
|
||||
.Build();
|
||||
|
||||
private static readonly JsonSerializerOptions JsonSerializerOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
AllowTrailingCommas = true,
|
||||
};
|
||||
|
||||
private const string MetaFence = "meta";
|
||||
|
||||
|
||||
[Test]
|
||||
public void PostsDirectory_ShouldContainIndexFiles()
|
||||
{
|
||||
var postsDir = GetPostsDirectory();
|
||||
var files = Directory.GetFiles(postsDir, "index.md", SearchOption.AllDirectories);
|
||||
|
||||
files.Should().NotBeEmpty("The wwwroot/posts directory should contain blog posts with index.md files.");
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(GetPostIndexFiles))]
|
||||
public async Task IndexMd_Metadata_CanBeProperlyParsedAndRead(string filePath)
|
||||
{
|
||||
var markdown = await File.ReadAllTextAsync(filePath);
|
||||
var document = Markdown.Parse(markdown, MarkdownPipeline);
|
||||
|
||||
var metadataBlock = document
|
||||
.OfType<FencedCodeBlock>()
|
||||
.FirstOrDefault(block =>
|
||||
block.Arguments is not null &&
|
||||
block.Arguments.Contains(MetaFence)
|
||||
);
|
||||
|
||||
metadataBlock.Should().NotBeNull($"file '{filePath}' should contain a fenced code block with '{MetaFence}' argument.");
|
||||
|
||||
var jsonContent = metadataBlock.Lines.ToString();
|
||||
|
||||
jsonContent.Should().NotBeNullOrWhiteSpace($"metadata code block in '{filePath}' should not be empty.");
|
||||
|
||||
Post? post = null;
|
||||
|
||||
Action deserializeAction = () => post = JsonSerializer.Deserialize<Post>(jsonContent, JsonSerializerOptions);
|
||||
|
||||
deserializeAction.Should().NotThrow($"JSON metadata in '{filePath}' should be valid JSON matching the Post record schema.");
|
||||
|
||||
post.Should().NotBeNull($"deserialized Post from '{filePath}' should not be null.");
|
||||
post.Title.Should().NotBeNullOrWhiteSpace($"post in '{filePath}' must have a title.");
|
||||
post.Lead.Should().NotBeNullOrWhiteSpace($"post in '{filePath}' must have a lead summary.");
|
||||
post.PublishedAt.Should().NotBe(default, $"post in '{filePath}' must have a valid published date.");
|
||||
post.OpenGraphImage.Should().NotBeNullOrWhiteSpace($"post in '{filePath}' must have an openGraphImage path.");
|
||||
}
|
||||
|
||||
private static string GetPostsDirectory()
|
||||
{
|
||||
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
|
||||
while (current is not null)
|
||||
{
|
||||
var candidate = Path.Combine(current.FullName, "src", "Blog", "wwwroot", "posts");
|
||||
|
||||
if (Directory.Exists(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException($"Could not locate 'src/Blog/wwwroot/posts' starting from '{AppContext.BaseDirectory}'.");
|
||||
}
|
||||
|
||||
private static IEnumerable<TestCaseData> GetPostIndexFiles()
|
||||
{
|
||||
var postsDir = GetPostsDirectory();
|
||||
var files = Directory.GetFiles(postsDir, "index.md", SearchOption.AllDirectories);
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
var relativePath = Path.GetRelativePath(postsDir, file);
|
||||
yield return new TestCaseData(file).SetName($"PostMetadata_{relativePath.Replace('\\', '/')}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user