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
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ dotnet_naming_rule.parameters_rule.severity = warning
dotnet_diagnostic.CA1515.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/MicrosoftCodeAnalysis/CA1515.md
dotnet_diagnostic.CA1707.severity = error # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/MicrosoftCodeAnalysis/CA1707.md
dotnet_diagnostic.CA2007.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/MicrosoftCodeAnalysis/CA2007.md

dotnet_diagnostic.CA5394.severity = none

# SonarAnalyzer.CSharp
# https://rules.sonarsource.com/csharp
Expand Down
25 changes: 24 additions & 1 deletion docs/Setup/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,27 @@ through the possibilities.
- [Docker](Docker.md)

### Updates
Going from one version to another can introduce major breaking changes. The [release page](https://github.com/linkdotnet/Blog/releases) or [`MIGRATION.md`](../../MIGRATION.md) will show the steps to migrate.
Going from one version to another can introduce major breaking changes. The [release page](https://github.com/linkdotnet/Blog/releases) or [`MIGRATION.md`](../../MIGRATION.md) will show the steps to migrate. In newer version automatic is possible (Entity Framework for SQL-based databases) and a custom migration tool for `appsettings.json` is provided.

## Local Development
To spin up some mock data for local development, use the following method when registering services:
```csharp
if (builder.Environment.IsDevelopment())
{
// This fakes the whole authentication process and logs in every user automatically.
builder.Services.UseDummyAuthentication();

// This seeds some dummy data into the database for local development.
// It also overrides any real database configuration to use an in-memory database.
// So on restart (or removing this line) all data will be lost.
builder.Services.UseDummyData();
}
```

`UseDummyData` comes with a configuration:
```csharp
builder.Services.UseDummyData(options =>
{
options.NumberOfBlogPosts = 15; // Default is 20
});
```
42 changes: 42 additions & 0 deletions src/LinkDotNet.Blog.Web/Features/DummyData/DummyDataExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using LinkDotNet.Blog.Infrastructure.Persistence;
using LinkDotNet.Blog.Infrastructure.Persistence.Sql;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;

namespace LinkDotNet.Blog.Web.Features.DummyData;

public static class DummyDataExtensions
{
/// <summary>
/// This will seed some blog post data and replace the connection to the real database with an in-memory database with dummy data.
/// Use this for testing or development purposes only.
/// </summary>
public static void UseDummyData(this IServiceCollection services, DummyDataOptions? options = null)
{
ArgumentNullException.ThrowIfNull(services);

var descriptors = services.Where(d => d.ServiceType == typeof(IRepository<>)).ToList();
foreach (var descriptor in descriptors)
{
services.Remove(descriptor);
}

var dummyDataOptions = options ?? new DummyDataOptions();

services.AddPooledDbContextFactory<BlogDbContext>(builder =>
{
builder.UseSqlite("DataSource=file::memory:?cache=shared")
#if DEBUG
.EnableDetailedErrors()
#endif
;
});

services.AddScoped(typeof(IRepository<>), typeof(Repository<>));

services.AddSingleton(dummyDataOptions);
services.AddHostedService<DummyDataSeeder>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace LinkDotNet.Blog.Web.Features.DummyData;

public sealed class DummyDataOptions
{
/// <summary>
/// Gets or sets the number of blog posts to seed.
/// </summary>
public int NumberOfBlogPosts { get; set; } = 20;
}
128 changes: 128 additions & 0 deletions src/LinkDotNet.Blog.Web/Features/DummyData/DummyDataSeeder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using LinkDotNet.Blog.Domain;
using LinkDotNet.Blog.Infrastructure.Persistence;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace LinkDotNet.Blog.Web.Features.DummyData;

internal sealed class DummyDataSeeder : IHostedService
{
private readonly IServiceProvider serviceProvider;
private readonly DummyDataOptions options;

public DummyDataSeeder(IServiceProvider serviceProvider, DummyDataOptions options)
{
this.serviceProvider = serviceProvider;
this.options = options;
}

public async Task StartAsync(CancellationToken cancellationToken)
{
using var scope = serviceProvider.CreateScope();
var repository = scope.ServiceProvider.GetRequiredService<IRepository<BlogPost>>();

var blogPosts = GenerateDummyBlogPosts(options.NumberOfBlogPosts);
await repository.StoreBulkAsync(blogPosts);
}

public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;

private static List<BlogPost> GenerateDummyBlogPosts(int count)
{
const string loremIpsum =
"""
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

## Code Example

```csharp
public class Example
{
public void HelloWorld()
{
Console.WriteLine("Hello, World!");
}
}
```

## More Content

Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.
""";

var titles = new[]
{
"Getting Started with C# 12",
"Understanding Blazor Components",
"ASP.NET Core Best Practices",
"Mastering Entity Framework Core",
"Introduction to LINQ",
"Building RESTful APIs",
"Dependency Injection in .NET",
"Working with SignalR",
"Testing in .NET with xUnit",
"Advanced C# Features",
"Docker and .NET Applications",
"Microservices Architecture",
"Azure DevOps CI/CD",
"Authentication and Authorization",
"Performance Optimization Tips",
"Modern Web Development",
"Clean Code Principles",
"Design Patterns in C#",
"Asynchronous Programming",
"Working with Databases",
};

var tags = new[]
{
"CSharp",
"Blazor",
"ASP.NET",
"EntityFramework",
"LINQ",
"WebAPI",
"Testing",
"Docker",
"Azure",
"Architecture",
};

var blogPosts = new List<BlogPost>();

for (var i = 0; i < count; i++)
{
var title = i < titles.Length ? titles[i] : $"Blog Post {i + 1}";
var shortDescription = $"This is a dummy blog post about {title}. It contains valuable information for developers.";

var selectedTags = tags
.OrderBy(_ => Random.Shared.Next())
.Take(Random.Shared.Next(1, 4))
.ToArray();

var blogPost = BlogPost.Create(
title,
shortDescription,
loremIpsum,
"https://via.placeholder.com/800x400",
true,
DateTime.UtcNow.AddDays(-i),
null,
selectedTags,
null,
"Dummy Author");

blogPost.Likes = Random.Shared.Next(0, 10);
blogPosts.Add(blogPost);
}

return blogPosts;
}
}
16 changes: 9 additions & 7 deletions src/LinkDotNet.Blog.Web/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using HealthChecks.UI.Client;
using LinkDotNet.Blog.Web.Authentication.OpenIdConnect;
using LinkDotNet.Blog.Web.Authentication.Dummy;
using LinkDotNet.Blog.Web.Features.DummyData;
using LinkDotNet.Blog.Web.RegistrationExtensions;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
Expand Down Expand Up @@ -49,13 +50,14 @@ private static void RegisterServices(WebApplicationBuilder builder)
.AddBlazoriseWithBootstrap()
.AddResponseCompression()
.AddHealthCheckSetup();

builder.Services.AddAntiforgery();

if (builder.Environment.IsDevelopment())
{
builder.Services.UseDummyAuthentication();
}
if (builder.Environment.IsDevelopment())
{
builder.Services.UseDummyAuthentication();
builder.Services.UseDummyData();
}
else
{
builder.Services.UseAuthentication();
Expand Down Expand Up @@ -87,14 +89,14 @@ private static void ConfigureApp(WebApplication app)
.RequireAuthorization();

app.UseStatusCodePagesWithReExecute("/NotFound");

app.UseRouting();

app.UseUserCulture();

app.UseAuthentication();
app.UseAuthorization();

app.UseAntiforgery();

app.UseRateLimiter();
Expand Down