Utilizing .NET Core 2.2 Web API as a Backend for a Webblog

For the longest time, I've thought about creating my own little place online, to share my thoughts and my journey as a freelance dotnet developer and consultant. After well, heck 10 years we are finally here!

Post 1

The last many years, I haven’t really paid attention to what was going on in the frontend world of web development. I’ve primarily been focused on integration projects and cloud-based microservices running on the Microsoft Azure platform.

I quickly discovered that a lot has changed in the frontend space. Rather than locking myself into a specific frontend framework from the beginning, I decided to build a clean .NET Web API backend first. That way, the frontend technology becomes a replaceable concern.

By abstracting the backend behind an API service, we stay fully compatible with whatever frontend strategy or technology stack we decide to use. The trade-off is a bit more architectural overhead, but since modern systems are increasingly moving away from monolithic structures and toward service-oriented designs, this separation makes sense. With the philosophical reasoning in place, let’s get into some technical details.

The backend is built using modern .NET (8+). Instead of using mutable classes, we model our data using records, which better represent immutable data structures and provide value-based equality.


public sealed record BlogPost
{
    public Guid Id { get; init; } = Guid.NewGuid();
    public DateTime CreateDate { get; init; } = DateTime.UtcNow;

    public required string Title { get; init; }
    public required string Content { get; init; }

    public string? Summary { get; init; }
    public string? SmallCoverUrl { get; init; }
    public string? MediumCoverUrl { get; init; }
    public string? BigCoverUrl { get; init; }

    public Enums.BlogPostCategory Category { get; init; }

    public IEnumerable<string> Tags { get; init; } = [];
}

Notice the use of the required keyword. This ensures that mandatory fields such as Title and Content must be initialized at compile time. Combined with nullable reference types, this makes our API contracts explicit and type-safe instead of relying purely on runtime validation attributes.

For storage, I’m still using a temporary JSON file to simulate persistence. In a real-world scenario, I would likely use MongoDB or another document database.

Modern .NET no longer requires a Startup.cs file. Instead, everything is configured directly in Program.cs.


var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.MapControllers();

app.Run();

OpenAPI (Swagger) is built directly into the platform and provides automatic documentation of the API surface. For JSON serialization, we rely on the built-in System.Text.Json, which is now highly optimized and sufficient for most use cases.

With the basics in place, let’s look at the controller implementation. We’ll keep it simple: one endpoint returns all blog posts, and one returns a single post by Id.


[Route("api/[controller]")]
[ApiController]
public class BlogPostController : ControllerBase
{
    [HttpGet]
    public ActionResult<IEnumerable<BlogPost>> Get()
    {
        var dbService = new Services.DbService();
        var db = dbService.GetDb("db.json");

        if (db == null || !db.BlogPosts.Any())
        {
            db = new Models.Db
            {
                BlogPosts = new List<BlogPost>
                {
                    new BlogPost
                    {
                        Title = "Blog Sample",
                        Content = "Hello World",
                        Summary = "Sample summary",
                        SmallCoverUrl = "http://placehold.it/720x432",
                        MediumCoverUrl = "http://placehold.it/1120x640",
                        BigCoverUrl = "http://placehold.it/1920x1120",
                        Category = Enums.BlogPostCategory.Code,
                        Tags = new[] { "tag1", "tag2" }
                    }
                }
            };

            dbService.SaveDb(db, "db.json");
        }

        return db.BlogPosts.ToList();
    }

    [HttpGet("{id:guid}")]
    public ActionResult<BlogPost> Get(Guid id)
    {
        var dbService = new Services.DbService();
        var db = dbService.GetDb("db.json");

        var result = db?.BlogPosts.SingleOrDefault(x => x.Id == id);

        if (result is null)
            return NotFound($"BlogPost with Id '{id}' not found.");

        return result;
    }
}

Because we use required properties, the compiler guarantees that Title and Content are always initialized. This improves correctness compared to older patterns where properties were nullable and enforced only via validation attributes.

For a production-ready solution, the database service would of course be injected via dependency injection rather than instantiated directly inside the controller. We’ll cover proper dependency injection and architectural layering in a future post.

Thanks for reading, and feedback is always welcome.