Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
158754 stories
·
33 followers

Every software company will become a dev tools company

1 Share
Abstract dark digital render featuring sharp glowing golden geometric shards, representing platform engineering and AI code generation complexity.

If most software will soon be built by machines, the job of software engineers will change: from writing software to building the machines that write it.

Soon, engineers won’t be classified as frontend or backend, but as product engineers, who build the customer-facing product, and platform engineers, who build the tools those product engineers use. And since more and more code will be written by machines, we’ll need more platform engineers to build and fine-tune those machines.

“Call it harness engineering, call it loop engineering. It’s still platform engineering, and it’s building the tools every engineer depends on to write code at all.”

I’ve been thinking about it ever since the tokenmaxxing fiasco, and every time I read one of those reports showing that faster code generation, more often than not, just creates more friction downstream. Every team is wiring up its own prompts, its own guardrails, its own dashboard for tracking what the agent got wrong, none of it shared, all of it repeated across the org. Fragmentation, not speed, is what a hundred engineers each solving the same platform problem alone gets you.

Platform engineering, per its definition, means building the shared toolchains and workflows that let engineering orgs serve themselves, usually packaged into one internal developer platform covering everything an application needs across its life cycle. In plain terms, it’s developers building tools for other developers so the rest of engineering can ship with less friction. If you’re helping developers ship software with less friction, congratulations! You’re already doing some form of platform engineering.

Two teams converging into one

I recently spoke to Vanitha Kumar, a technology consultant at ThoughtWorks, who’d arrived at a similar realization from a different angle. She and a colleague were mapping a team topology diagram for a client and found themselves drawing two separate boxes labeled “platform”: the familiar one for CI/CD and delivery infrastructure, and a newer one named “agentic developer platform,” which frameworks and models developers are allowed to use, where AI cost controls live, and where the guardrails for agentic development sit. Partway through, they realized it wasn’t going to stay two boxes. 

It converges into one platform that owns delivery infrastructure and agentic guardrails together, and that platform’s reach keeps moving earlier into the lifecycle. Platform engineering used to start at the first commit. Now it starts before any code exists.

For the last two years, AI coding tools have drastically changed the job of classic software engineers, the people writing application code by hand. Most platform engineering work was already automated: infrastructure as code, scripted pipelines, and not much manual coding for an agent to accelerate.

“Every company that ships software is, in effect, becoming a dev tools company.”

Now that AI is generating application code at scale, the team that has to build the machinery to control, verify, and scale that code generation is platform engineering. Every company that ships software is, in effect, becoming a dev tools company: building the tooling its own engineers need to work with agents safely, with the same seriousness it builds anything it sells externally.

Harness engineering is platform engineering for AI

Harness engineering, or building and maintaining the harness, the feedback loops, the guardrails, and the context an agent needs to work safely in a codebase, is just platform engineering with an AI-specific layer on top of what platform teams already know how to do.

We can invent new terms like harness engineering, loop engineering, or software factories – that’s all just platform engineering, the discipline of making the process of building software easier.

Platform engineering for AI engineering could, for example, mean working on building your engineering org’s anti-AI slop register: a list of Invariants, a list of rules specific to your codebase that automatically apply to every matching change. That list usually captures something your team learned the hard way, usually as a recurring review comment, so reviewers don’t have to flag it again. Developers don’t need to include them in the specs because the system automatically loads the matching set.

A good harness does two things: it raises the odds the agent gets the task right on the first pass, and it gives the agent a way to catch and fix its own mistakes before a human ever sees them. Done well, that cuts review toil, raises the quality of what ships, and saves tokens.

If you skip that work, agents will keep making the same mistake forever, and you will only ever catch problems after they happen, not before. Frontier models give engineers the tools to generate code faster, but they still require platform teams to assemble, guardrail, and harness them, and to prevent them from becoming a faster way of shipping slop.

What platform engineering now owns

The scope is bigger than keeping the agent tooling running. It includes which models and frameworks get approved because that decision carries security and cost implications. It includes usage and scaling because rolling agentic coding out to hundreds of engineers looks nothing like rolling it out to ten. 

It includes cost because unmanaged usage gets expensive fast. It includes authorization because an agent acting on an engineer’s behalf isn’t the same as the engineer, and handing it identical access is a mistake a lot of orgs are already making.

And it includes the harness itself: the feedback loops that take what a security scanner or a linter finds and route it back into the agent instead of just failing a build.

None of that is product engineering’s job. It’s platform engineering becoming what a growing part of the industry has started calling the control plane: the layer that decides what every agent in the org is allowed to touch and how.

This is where the org charts will start to change. Building and maintaining the harness, the feedback loops, the guardrails, and the context an agent needs to work safely in a codebase is ongoing work, not a project with an end date. 

Platform engineering was long seen as a supporting function, useful but never the center of gravity. That will change once the platform owns everything above. In plenty of orgs, more engineers will end up on the platform side, building and maintaining the harness, than on the product side, because the harness is now the harder part of the job.

“Anthropic or OpenAI give you the raw agent, the ‘engine.’ But that engine has no idea what your codebase looks like.”

Anthropic or OpenAI give you the raw agent, the “engine.” But that engine has no idea what your codebase looks like, what conventions your team follows, how much risk you’re willing to take, or what your budget is. Turning it into something that respects those specifics is work only the platform team can do. 

If nobody does that work deliberately, it still happens, just badly: each team builds its own patchwork version of the same governance layer, with nobody clearly responsible when it breaks.

Building the factory that builds software

When machines write most of the code, the productivity gains come from how strong the machinery is, and broken tooling means you can’t ship quality software no matter how good the people running it are.

How productive an engineering org becomes with AI has nothing to do with which model it’s paying for, how strict the adoption mandate is, or how fast it chases whatever’s trending — loop engineering this month, graph engineering the next. 

It comes down to one thing: whether the people building the tools, the platform engineers, are treated as a supporting function.

The post Every software company will become a dev tools company appeared first on The New Stack.

Read the whole story
alvinashcraft
46 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

How a GitHub lawyer uses Copilot CLI to verify AI outputs

1 Share
From: GitHub
Duration: 1:38
Views: 43

When the cost of error is high, trust is everything. Dan, Director, Senior Corporate Counsel at GitHub, used GitHub Copilot CLI to build a custom skill called Eyeball that embeds inline screenshots of source documents to verify AI analysis. Watch how this tool helps the legal team check complex clauses and act on AI outputs with confidence. Now, every lawyer at GitHub is using the terminal to streamline their daily workflows.

#GitHubCopilot #CopilotCLI #AI

Stay up-to-date on all things GitHub by connecting with us:

YouTube: https://gh.io/subgithub
Blog: https://github.blog
X: https://twitter.com/github
LinkedIn: https://linkedin.com/company/github
Insider newsletter: https://resources.github.com/newsletter/
Instagram: https://www.instagram.com/github
TikTok: https://www.tiktok.com/@github

About GitHub
It’s where over 180 million developers create, share, and ship the best code possible. It’s a place for anyone, from anywhere, to build anything—it’s where the world builds software. https://github.com

Read the whole story
alvinashcraft
47 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Governance Is a Developer Experience Problem

1 Share

This is the third post of a 3-part series by Docker Captain Karan Verma. Catch up on Part 1: Your Laptop Is the New Production Environment and Part 2: Runtime Enforcement, Not Runtime Advice.

The conversation around AI governance often starts with security. That’s understandable. When autonomous systems can execute commands, access tools, and interact with production-adjacent environments, organizations naturally focus on risk. But after spending time thinking about agent workflows, I’ve become convinced that governance is about more than security. It’s also a developer experience problem.

The Trust Bottleneck

Most organizations don’t struggle to adopt new tools because the tools are incapable. They struggle because the organization doesn’t trust them yet. The history of software development is full of examples. Cloud adoption accelerated when organizations became comfortable with cloud governance. Containers accelerated when teams gained confidence in isolation and operational controls. CI/CD accelerated when organizations trusted automated deployment pipelines. The pattern repeats. Capability arrives first. Trust arrives later. Adoption follows trust. AI agents are no different.

image1 2

Caption: Capability alone does not drive adoption. Trust enables organizations to delegate work, expand usage, and realize productivity gains.

The Wrong Tradeoff

Governance is often framed as a choice between speed and control. Move fast and accept risk. Or add controls and slow everyone down. In practice, the most successful developer platforms rarely make this tradeoff. Instead, they create environments where developers can move quickly because boundaries already exist. A developer deploying through a mature platform doesn’t need to think about every networking rule, access policy, or infrastructure safeguard every time they ship code. The platform already provides those guarantees. The same principle applies to agent systems. The goal isn’t to force developers to manually approve every action. The goal is to create environments where useful actions can happen safely by default.

A Tale of Two Teams

Imagine two engineering teams using the same coding agent. The first team allows agent usage only in limited experiments because nobody is completely certain what the agent can access, execute, or modify. Every new workflow requires additional review. Every new capability triggers a discussion about risk.

The second team operates within clearly defined boundaries around execution, tools, and credentials. Developers understand where agents run, what systems they can access, and how activity is observed.

The underlying model is identical. The difference is trust. Over time, that difference may matter more than the model itself. Organizations rarely scale technology they do not trust.

Why Boundaries Create Freedom

This idea sounds counterintuitive at first. Boundaries feel restrictive. But in software systems, boundaries often enable autonomy rather than limiting it.

When organizations know:

  • where agents run,
  • what agents can access,
  • which tools agents can use,
  • how activity is observed,

They become more comfortable delegating work. Without those boundaries, every workflow becomes an exception process. Every deployment requires discussion. Every new capability triggers concern. Every new tool requires negotiation. Governance reduces uncertainty. Reducing uncertainty increases trust. And trust enables adoption.

The Platform Shift

One thing that stands out in recent discussions around agent infrastructure is that governance is increasingly moving into the platform itself. Developers shouldn’t need to become security experts every time they use an agent. Just as developers rely on platforms to handle identity, networking, deployment, and observability concerns, governance increasingly becomes part of the environment where agents operate. When governance is embedded into the platform, developers spend less time worrying about boundaries and more time focusing on outcomes. That’s a developer experience improvement as much as a security improvement.

Governance as an Enabler

The organizations that adopt agents most successfully may not be the organizations with the fewest controls. They may be the organizations with the clearest controls. Clear boundaries create confidence. Confidence enables delegation. Delegation unlocks productivity. Viewed through that lens, governance is not the thing slowing agent adoption. It is one of the things that makes large-scale adoption possible.

Looking Ahead

The conversation around AI agents often focuses on what models can do. Increasingly, I think the more interesting question is what organizations are willing to trust them to do. That trust won’t come from capability alone. It will come from visibility, accountability, and well-defined boundaries because the future of agentic software is unlikely to be determined solely by the most capable agents. It will also be shaped by the environments that make those agents trustworthy enough to use at scale.

Learn more

Read the whole story
alvinashcraft
1 hour ago
reply
Pennsylvania, USA
Share this story
Delete

"If the code works, why does clean code matter?"

1 Share

Hello and Welcome, I’m your Code Monkey!

"If the code works, why does clean code matter?"

This is probably the most common argument I hear against clean code. And honestly, if you’re making a tiny one-day prototypes, maybe yeah it doesn't matter much since the project is too small to cause problems. But the longer the project goes, the more important it becomes!

The real question is not "does the code work today?"

The real question is "will the code still work after I add 20 new features?"

Messy code is fine when the project is tiny because you can keep everything in your head. Even if you are a beginner it’s not hard to keep 10 classes and 100 lines of code in your head. But as soon as the game grows, that stops being possible, you cannot keep a mental model of 1000 classes and 10,000 lines of code. On that scale, every new feature becomes slower to build. Every bug becomes harder to track. Every change creates fear because you don’t know what you might break.

That's when the project starts falling apart and that's when people usually quit and the project ends up as yet another one in the folder titled "AbandonedProjects"

Clean code is not about making your code pretty! Instead it’s about making sure you can keep working on the game tomorrow, next week, and 6 months from now and actually release it! Disposable prototypes can get away with anything but finished games need maintainable code.

So if you want to actually finish and release your games, I highly recommend you focus on writing the best quality code you can write.

Check out my talk on YouTube titled: "How to Write High Quality Code that doesn't fall apart" In this talk I cover a very detailed overview of what clean code is (and what isn’t) as well as some very practical guides for how you can write better code that enables you to build better games and actually finish them.

Thanks for reading! Best of luck in your game dev journey!



Get Rewards by Sending the Game Dev Report to a friend!

(please don’t try to cheat the system with temp emails, it won’t work, just makes it annoying for me to validate)

Thanks for reading!

Code Monkey

Read the whole story
alvinashcraft
1 hour ago
reply
Pennsylvania, USA
Share this story
Delete

Creating a Production-Ready CRUD Application in ASP.NET Core

1 Share

Are you a beginner looking to create a portfolio to impress recruiters? Or perhaps an experienced developer looking to build better basic apps ready to evolve? Learn how to create a CRUD application in ASP.NET Core that goes beyond the basics and has everything you need to stand out in the real world.

In this post, you’ll learn how to structure an ASP.NET Core project following CRUD best practices, even when the premise is simple. The idea here is to build something you can actually use as a portfolio.

In addition, we’ll focus on decisions that experienced developers make daily: how to avoid unnecessary coupling, how to better model the domain, and how to prepare your application to grow without becoming chaotic.

By the end of the post, you’ll have a solid foundation on how to create a CRUD application ready to evolve into something bigger, maybe with authentication, messaging, caching, or even an event-driven architecture.

The Problem with Generic CRUDs

You’ve probably faced a requirement like this at some point: Create a simple, straightforward CRUD that only performs the basic operations of Create, Read, Update and Delete, with entities representing tables, and everything working in a few minutes. And that’s not wrong. The problem starts when this same model, designed for prototyping, is taken to production without being prepared for future evolution.

One of the clearest signs of a basic CRUD is the use of anemic entities, classes that only have properties, without any behavior:

public class User
{
    public string Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}

Here, the entity does not protect its own state. Therefore, it can be created with an empty name, it can have an invalid email, it can be changed from anywhere. In other words, there is no business rule, just a simple data structure.

Another classic symptom of a prematurely scaled CRUD is having rules and validations scattered throughout the code. In basic CRUDs, when rules appear, they are scattered a bit in the Controller class, a bit in the Service class, and in the worst cases they don’t even exist.

Consider the example below:

   if (string.IsNullOrWhiteSpace(user.Name))
        return Results.BadRequest();

This solves the immediate problem, but creates a bigger one: the rule doesn’t belong to the domain, it’s loose in the system. Therefore, as the system grows, validations can be duplicated, lose consistency and become harder to find everywhere because they are scattered.

The biggest problem here is that you’ve only modeled the data, not the system. Basic CRUDs focus on basic operations: creating, reading, updating and deleting data. But real systems are about behavior: A user can register, they can deactivate their account, they can change their email based on internal validations, they can have a preferred name, they can change their address …

When you only model data, the system loses meaning and becomes just a data handling tool, which can easily be replaced by anything else, like a simple Excel spreadsheet for example.

The opposite of this are applications that, despite using only basic operations, are prepared for new features that will likely be needed. The image below illustrates the main points of both approaches:

Creating a CRUD Ready for Production

Now that we’ve reviewed some examples to avoid, let’s see how to create a complete CRUD application, organizing each part of the code according to best practices and preparing the project for evolution. You can access the complete project code in this GitHub repository: Campus Hub source code.

Architecture and Basic Structure

The application will be a CRUD to manage university courses, which can be registered, updated and deactivated. We will use the principles of Clean Architecture combined with tactical Domain-Driven Design (DDD) to create the basic structure of the project. Thus, the application will have the following project organization:

src/

  • Presentation -> CampusHub.Api
  • Application -> CampusHub.Application
  • Domain -> CampusHub.Domain
  • Infrastructure -> CampusHub.Infrastructure

Let’s run the .NET commands to create the projects. In a terminal, execute the command below:

dotnet new sln -n CampusHub

This will create a new solution (CampusHub.sln). Then, to create the projects within the src directory, run the following command:

dotnet new webapi -n CampusHub.Api -o src/CampusHub.Api
dotnet new classlib -n CampusHub.Application -o src/CampusHub.Application
dotnet new classlib -n CampusHub.Domain -o src/CampusHub.Domain
dotnet new classlib -n CampusHub.Infrastructure -o src/CampusHub.Infrastructure

Now, to add the projects to the solution, run the following command:

dotnet sln add src/CampusHub.Api/CampusHub.Api.csproj
dotnet sln add src/CampusHub.Application/CampusHub.Application.csproj
dotnet sln add src/CampusHub.Domain/CampusHub.Domain.csproj
dotnet sln add src/CampusHub.Infrastructure/CampusHub.Infrastructure.csproj

Finally, run the following commands to add the dependencies between the projects:

dotnet add src/CampusHub.Application reference src/CampusHub.Domain
dotnet add src/CampusHub.Infrastructure reference src/CampusHub.Domain
dotnet add src/CampusHub.Infrastructure reference src/CampusHub.Application
dotnet add src/CampusHub.Api reference src/CampusHub.Application
dotnet add src/CampusHub.Api reference src/CampusHub.Infrastructure

Then, in the CampusHub.Infrastructure.cs add the following NuGet packages:


  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0" />
  </ItemGroup>

Domain Layer

In the domain layer, we will place the project’s entities and enums. Therefore, we will have a class to represent the Course entity and an enum to represent the course statuses. So, within the CampusHub.Domain project, create a folder called Entities and add the following class inside it:

using CampusHub.Domain.Enums;

namespace CampusHub.Domain.Entities;

public class Course
{
    private const int MinNameLength = 3;
    private const int MaxNameLength = 200;
    private const int MinWorkload = 1;

    public Guid Id { get; private set; }
    public string Code { get; private set; } = string.Empty;
    public string Name { get; private set; } = string.Empty;
    public int WorkloadHours { get; private set; }
    public int MaxStudents { get; private set; }
    public CourseStatus Status { get; private set; }

    // EF Core requirement
    private Course() { }

    private Course(string code, string name, int workloadHours, int maxStudents)
    {
        Id = Guid.NewGuid();
        SetCode(code);
        SetName(name);
        SetWorkload(workloadHours);
        SetMaxStudents(maxStudents);

        Status = CourseStatus.Draft;
    }

    public static Course Create(string code, string name, int workloadHours, int maxStudents)
    {
        return new Course(code, name, workloadHours, maxStudents);
    }

    // Behavior methods
    public void UpdateDetails(string name, int workloadHours, int maxStudents)
    {
        EnsureNotArchived();

        SetName(name);
        SetWorkload(workloadHours);
        SetMaxStudents(maxStudents);
    }

    public void ChangeCapacity(int maxStudents)
    {
        EnsureNotArchived();

        SetMaxStudents(maxStudents);
    }

    public void Activate()
    {
        if (Status == CourseStatus.Active)
            throw new InvalidOperationException("Course is already active.");

        Status = CourseStatus.Active;
    }

    public void Archive()
    {
        if (Status == CourseStatus.Archived)
            throw new InvalidOperationException("Course is already archived.");

        Status = CourseStatus.Archived;
    }


    // Private validation logic
    private void SetCode(string code)
    {
        if (string.IsNullOrWhiteSpace(code))
            throw new ArgumentException("Course code cannot be empty.");

        Code = code.Trim().ToUpper();
    }

    private void SetName(string name)
    {
        if (string.IsNullOrWhiteSpace(name))
            throw new ArgumentException("Course name cannot be empty.");

        if (name.Length < MinNameLength || name.Length > MaxNameLength)
            throw new ArgumentException($"Course name must be between {MinNameLength} and {MaxNameLength} characters.");

        Name = name.Trim();
    }

    private void SetWorkload(int workloadHours)
    {
        if (workloadHours < MinWorkload)
            throw new ArgumentException("Workload must be greater than zero.");

        WorkloadHours = workloadHours;
    }

    private void SetMaxStudents(int maxStudents)
    {
        if (maxStudents <= 0)
            throw new ArgumentException("Max students must be greater than zero.");

        MaxStudents = maxStudents;
    }

    private void EnsureNotArchived()
    {
        if (Status == CourseStatus.Archived)
            throw new InvalidOperationException("Archived courses cannot be modified.");
    }
}

Note that this entity has an excellent structure. It is not anemic, meaning it has behaviors such as the Activate(), Archive() and UpdateDetails() methods.

Furthermore, you can never create an invalid Course because the verification methods protect against creating an invalid state. For example, an archived course cannot be modified, and an active course cannot be reactivated.

Now, create a new folder called Enums and add the following enum to it:

namespace CampusHub.Domain.Enums;

public enum CourseStatus
{
    Draft = 0,
    Active = 1,
    Archived = 2
}

Application Layer

The application layer is where we will configure the service classes with CRUD methods and Data Transfer Objects (DTOs). So, within the CampusHub.Application project, create a new folder called DTOs and create the following records inside it:

namespace CampusHub.Application.DTOs;

public record CourseResponseDto(
    Guid Id,
    string Code,
    string Name,
    int WorkloadHours,
    int MaxStudents,
    string Status
);
namespace CampusHub.Application.DTOs;

public record CreateCourseDto(
    string Code,
    string Name,
    int WorkloadHours,
    int MaxStudents
);
namespace CampusHub.Application.DTOs;

public record UpdateCourseDto(
    string Name,
    int WorkloadHours,
    int MaxStudents
);

Then, create a new folder called Interfaces and add the following interfaces to it:

using CampusHub.Domain.Entities;

namespace CampusHub.Application.Interfaces;

public interface ICourseRepository
{
    Task AddAsync(Course course);
    Task<Course?> GetByIdAsync(Guid id);
    Task<List<Course>> GetAllAsync();
    Task Update(Course course);
    Task<bool> ExistsByCodeAsync(string code);
}
using CampusHub.Application.DTOs;

namespace CampusHub.Application.Interfaces;

public interface ICourseService
{
    Task<Guid> CreateAsync(CreateCourseDto dto);
    Task UpdateAsync(Guid id, UpdateCourseDto dto);
    Task ActivateAsync(Guid id);
    Task ArchiveAsync(Guid id);
    Task<CourseResponseDto?> GetByIdAsync(Guid id);
    Task<List<CourseResponseDto>> GetAllAsync();
}

Finally, create a new folder called Services and add the class below to it:

using CampusHub.Application.Interfaces;
using CampusHub.Domain.Entities;
using CampusHub.Application.DTOs;

namespace CampusHub.Application.Services;

public class CourseService : ICourseService
{
    private readonly ICourseRepository _repository;

    public CourseService(ICourseRepository repository)
    {
        _repository = repository;
    }

    public async Task<Guid> CreateAsync(CreateCourseDto dto)
    {
        var exists = await _repository.ExistsByCodeAsync(dto.Code);
        if (exists)
            throw new InvalidOperationException("Course code already exists.");

        var course = Course.Create(
            dto.Code,
            dto.Name,
            dto.WorkloadHours,
            dto.MaxStudents
        );

        await _repository.AddAsync(course);

        return course.Id;
    }

    public async Task UpdateAsync(Guid id, UpdateCourseDto dto)
    {
        var course = await _repository.GetByIdAsync(id)
            ?? throw new InvalidOperationException("Course not found.");

        course.UpdateDetails(dto.Name, dto.WorkloadHours, dto.MaxStudents);

        await _repository.Update(course);
    }

    public async Task ActivateAsync(Guid id)
    {
        var course = await _repository.GetByIdAsync(id)
            ?? throw new InvalidOperationException("Course not found.");

        course.Activate();

        await _repository.Update(course);
    }

    public async Task ArchiveAsync(Guid id)
    {
        var course = await _repository.GetByIdAsync(id)
            ?? throw new InvalidOperationException("Course not found.");

        course.Archive();

        await _repository.Update(course);
    }

    public async Task<CourseResponseDto?> GetByIdAsync(Guid id)
    {
        var course = await _repository.GetByIdAsync(id);

        if (course is null)
            return null;

        return MapToResponse(course);
    }

    public async Task<List<CourseResponseDto>> GetAllAsync()
    {
        var courses = await _repository.GetAllAsync();

        return courses.Select(MapToResponse).ToList();
    }

    private static CourseResponseDto MapToResponse(Course course)
    {
        return new CourseResponseDto(
            course.Id,
            course.Code,
            course.Name,
            course.WorkloadHours,
            course.MaxStudents,
            course.Status.ToString()
        );
    }
}

Here we have all the methods we need to execute the CRUD functions. Note that to modify the state of the objects we use the methods created in the domain class.

Infrastructure Layer

Now let’s create the infrastructure layer, used for communication with external parts of the application such as external APIs and databases. Within the CampusHub.Infrastructure project, create a new folder called Data and add the following classes to it:

using CampusHub.Domain.Entities;
using Microsoft.EntityFrameworkCore;

namespace CampusHub.src.CampusHub.Infrastructure.Data;

public class AppDbContext : DbContext
{
    public DbSet<Course> Courses => Set<Course>();

    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);

        base.OnModelCreating(modelBuilder);
    }
}
using CampusHub.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace CampusHub.src.CampusHub.Infrastructure.Data;

public class CourseConfiguration : IEntityTypeConfiguration<Course>
{
    public void Configure(EntityTypeBuilder<Course> builder)
    {
        builder.ToTable("Courses");

        builder.HasKey(c => c.Id);

        builder.Property(c => c.Code)
            .IsRequired()
            .HasMaxLength(20);

        builder.HasIndex(c => c.Code)
            .IsUnique();

        builder.Property(c => c.Name)
            .IsRequired()
            .HasMaxLength(200);

        builder.Property(c => c.WorkloadHours)
            .IsRequired();

        builder.Property(c => c.MaxStudents)
            .IsRequired();

        builder.Property(c => c.Status)
            .IsRequired()
            .HasConversion<int>(); // Enum -> int

        builder.Property(c => c.Id)
            .ValueGeneratedNever();
    }
}

Here we made the necessary configurations for the EF Core implementation, defining database details such as the table name, maximum number of characters for the Name property and others.

Next, create a new folder called Repositories and add the following class to it:

using CampusHub.Application.Interfaces;
using CampusHub.Domain.Entities;
using CampusHub.src.CampusHub.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;

namespace CampusHub.Infrastructure.Repositories;

public class CourseRepository : ICourseRepository
{
    private readonly AppDbContext _context;

    public CourseRepository(AppDbContext context)
    {
        _context = context;
    }

    public async Task AddAsync(Course course)
    {
        await _context.Courses.AddAsync(course);
        await _context.SaveChangesAsync();
    }

    public async Task<Course?> GetByIdAsync(Guid id)
    {
        return await _context.Courses.FirstOrDefaultAsync(c => c.Id == id);
    }

    public async Task<List<Course>> GetAllAsync()
    {
        return await _context.Courses.ToListAsync();
    }

    public async Task<bool> ExistsByCodeAsync(string code)
    {
        var normalizedCode = code.Trim().ToUpper();

        return await _context.Courses
            .AnyAsync(c => c.Code == normalizedCode);
    }

    public async Task Update(Course course)
    {
        _context.Courses.Update(course);
        await _context.SaveChangesAsync();
    }
}

In the repository class, we create the implementation of the CRUD methods that execute the operations on the database.

The last class in the infrastructure layer will be used to configure the dependency injection and define the connection string. So, inside the project add the class below:

using CampusHub.Application.Interfaces;
using CampusHub.Infrastructure.Repositories;
using CampusHub.src.CampusHub.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace CampusHub.Infrastructure;

public static class DependencyInjection
{
    public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddDbContext<AppDbContext>(options =>
        options.UseMySql(
            configuration.GetConnectionString("DefaultConnection"),
            ServerVersion.AutoDetect(configuration.GetConnectionString("DefaultConnection"))
        ));

        services.AddScoped<ICourseRepository, CourseRepository>();

        return services;
    }
}

API Layer

The API is the final layer of the application, here we will define the controller classes and other settings such as the database connection string. Inside the Controllers folder, add the following controller:

using CampusHub.Application.DTOs;
using CampusHub.Application.Interfaces;
using Microsoft.AspNetCore.Mvc;

namespace CampusHub.Api.Controllers;

[ApiController]
[Route("api/[controller]")]
public class CoursesController : ControllerBase
{
    private readonly ICourseService _service;

    public CoursesController(ICourseService service)
    {
        _service = service;
    }

    [HttpPost]
    public async Task<IActionResult> Create(CreateCourseDto dto)
    {
        var id = await _service.CreateAsync(dto);

        return CreatedAtAction(nameof(GetById), new { id }, null);
    }

    [HttpGet("{id:guid}")]
    public async Task<IActionResult> GetById(Guid id)
    {
        var course = await _service.GetByIdAsync(id);

        if (course is null)
            return NotFound();

        return Ok(course);
    }

    [HttpGet]
    public async Task<IActionResult> GetAll()
    {
        var courses = await _service.GetAllAsync();

        return Ok(courses);
    }

    [HttpPut("{id:guid}")]
    public async Task<IActionResult> Update(Guid id, UpdateCourseDto dto)
    {
        await _service.UpdateAsync(id, dto);

        return NoContent();
    }

    [HttpPost("{id:guid}/activate")]
    public async Task<IActionResult> Activate(Guid id)
    {
        await _service.ActivateAsync(id);

        return NoContent();
    }

    [HttpPost("{id:guid}/archive")]
    public async Task<IActionResult> Archive(Guid id)
    {
        await _service.ArchiveAsync(id);

        return NoContent();
    }
}

Then, in the Program class, replace the existing code with the code below:

using CampusHub.Application.Services;
using CampusHub.Application.Interfaces;
using CampusHub.Infrastructure;

var builder = WebApplication.CreateBuilder(args);

// Services

builder.Services.AddControllers();

builder.Services.AddEndpointsApiExplorer();

// Application

builder.Services.AddScoped<ICourseService, CourseService>();

// Infrastructure

builder.Services.AddInfrastructure(builder.Configuration);

// Build

var app = builder.Build();

// Middleware

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

Finally, in the file appsettings.json, add the following configuration. (Don’t forget to add your credentials!):

 "ConnectionStrings": {
    "DefaultConnection": "server=localhost;port=3306;database=campushub;user=YOUR_USER;password=YOUR_PASSWORD;"
  }
,

Running EF Core Commands

To apply the migration commands, in the project root, open a new terminal and run the commands below.

Creating the Migration Files

dotnet ef migrations add InitialCreate --project src/CampusHub.Infrastructure --startup-project src/CampusHub.Api --output-dir Data/Migrations

Applying the Migration Commands

dotnet ef database update --project src/CampusHub.Infrastructure --startup-project src/CampusHub.Api

What Does the Final Structure Look Like?

After implementing all the steps above, the project will have the following structure:

When Does a Complete CRUD Not Pay Off?

In this post, we created a complete CRUD, with all the necessary elements for its evolution. However, this complexity can exponentially increase development time and the level of knowledge required to maintain the project.

With this in mind, in some scenarios, simplicity is preferable. For example, when the system basically stores and retrieves data without relevant business rules. In scenarios such as basic registrations, auxiliary tables or internal administrative panels, adding layers such as services, rich entities and DDD patterns tends to generate more complexity than value, making the code more difficult to maintain without a justified need.

It is also not worthwhile to invest in a sophisticated architecture in the initial phases of a product, such as MVPs or systems still in validation. In these cases, the priority is speed and adaptation to frequent changes, and a simple structure allows for faster evolution without the burden of unnecessary abstractions.

On the other hand, a more elaborate CRUD makes sense when the domain has important rules, states, complex validations or when errors can have a significant impact. In these scenarios, a more structured architecture, as demonstrated in the post, helps protect the domain, maintain consistency and facilitate the system’s evolution over time (all parts are in their proper places).

The final decision of whether or not to use a complete and evolve-ready CRUD should therefore always take into account the level of complexity of the problem.

Conclusion and Next Steps

Knowing the best approach when creating new web applications is always a challenge, but we must keep in mind that if the project requires complex business rules, validations and errors that can have a significant impact, this is a strong indication that the application should not be a simple and generic CRUD.

In this post, we learned how to create a complete CRUD, with each layer representing a part of Clean Architecture, and using DDD development principles to implement a well-structured domain with private properties and behaviors.

But far beyond the basic structure presented in this post, a complete CRUD also involves unit tests, complex validations (FluentValidation), authentication and authorization with modern methods (JW Tokens for example), and many other elements. I hope this post serves as a starting point and helps you create CRUD applications that are not only ready to function, but also to evolve with quality over time.

Read the whole story
alvinashcraft
1 hour ago
reply
Pennsylvania, USA
Share this story
Delete

Build an Excel-Like Spreadsheet in .NET MAUI Using Blazor Hybrid

1 Share

TL;DR: Learn how to build an Excel‑like spreadsheet UI inside a .NET MAUI app using Blazor Hybrid to deliver consistent cross‑platform data editing, reusable web components, and a maintainable app architecture with native performance and better UX.

Delivering a consistent and modern user experience across mobile and desktop platforms has become essential for today’s business applications. With .NET MAUI Blazor Hybrid, you can build apps for web, desktop, Android, iOS, and macOS using a single shared codebase. This reduces duplication, simplifies maintenance, and significantly accelerates development.

In this blog, we’ll explore how to integrate the Syncfusion® Blazor Spreadsheet component into a .NET MAUI Blazor Hybrid application to deliver an Excel-like experience across all platforms.

Why use a Spreadsheet UI in a .NET MAUI App?

A spreadsheet interface makes sense when users need flexible, cell-based editing rather than row-by-row data entry. Compared to a standard DataGrid, a spreadsheet UI is better suited for:

  • Financial or budgeting apps where formulas are required
  • Data entry tools with copy/paste operations
  • Reporting or planning screens where users expect Excel-like behavior
  • Scenarios where users work with files rather than records

Using Blazor inside .NET MAUI also brings architectural benefits. The UI logic lives in reusable Razor components, while MAUI handles native shell features like navigation, lifecycle, and platform APIs. This approach reduces duplication and keeps the app easier to maintain as it grows.

Prerequisites

Before starting, make sure you have the following set up:

  • Visual Studio 2026 with .NET MAUI and ASP.NET workloads installed
  • Latest .NET SDK
  • Basic familiarity with .NET MAUI and Blazor

Once your environment is ready, the next step is to create a .NET MAUI Blazor Hybrid app and configure it to host a spreadsheet UI.

Create a .NET MAUI Blazor Hybrid App

Let’s begin by creating a new .NET MAUI Blazor Hybrid application. In Visual Studio, create a new project and select the .NET MAUI Blazor Hybrid App template.

Choose the .NET MAUI Blazor Hybrid App template
Choose the .NET MAUI Blazor Hybrid App template

Enter your project name, choose a location, configure the solution details, and then click Next.

Configure the project name and details
Configure the project name and details

On the next screen, select the target .NET Framework version you want to use and finish creating the project.

Selecting the target .NET framework version
Selecting the target .NET framework version

Once the project is created, you’ll see the standard MAUI Blazor Hybrid project structure. This includes:

  • A standard .NET MAUI project layout
  • A wwwroot folder for web assets
  • Razor components rendered through BlazorWebView
Default MAUI Blazor Hybrid project template
Default MAUI Blazor Hybrid project template

This project serves as the foundation for hosting Blazor-based UI components inside a native .NET MAUI application.

Install the required NuGet packages

With the project ready, the next step is to add the packages required for the Spreadsheet component. Install the following NuGet packages:

These packages provide the Spreadsheet control itself along with the theme files required for proper styling.

You can install them through the NuGet Package Manager or run the following commands in the Package Manager Console:

Install-Package Syncfusion.Blazor.Spreadsheet 
Install-Package Syncfusion.Blazor.Themes

After the installation is complete, you’re ready to register the services required by the Spreadsheet component.

Register Syncfusion services

Before using the Spreadsheet component, you need to register the Syncfusion Blazor services in your MAUI application. Open the MauiProgram.cs file and add the AddSyncfusionBlazor() service registration inside the CreateMauiApp method:

using Syncfusion.Blazor;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder.Services.AddSyncfusionBlazor();     
        ...
    }
}

This registration ensures that the required Syncfusion services are available when your application starts.

Add the required script and styles

Now that the services are registered, let’s add the resources required to render the Spreadsheet component correctly.

Open the wwwroot/index.html file and add the following CSS and JavaScript references inside the <head> section:

<head>
  ...
  <link href="_content/Syncfusion.Blazor.Themes/fluent2.css" rel="stylesheet" />
  <script src="_content/Syncfusion.Blazor.Spreadsheet/scripts/syncfusion-blazor-spreadsheet.min.js"></script>
</head>

The stylesheet provides the visual appearance of the component, while the JavaScript file enables the Spreadsheet’s interactive functionality.

Create the Spreadsheet page

With the application configured, it’s time to add the Spreadsheet component to your UI.
Open
Home.razor and add the following code:

@page "/"
@using Syncfusion.Blazor.Spreadsheet

<SfSpreadsheet @ref="Spreadsheet" DataSource="@DataSourceBytes">
    <SpreadsheetRibbon></SpreadsheetRibbon>
</SfSpreadsheet>

@code {
    private SfSpreadsheet? Spreadsheet;
    public byte[]?DataSourceBytes{ get; set; }

    protected override void OnInitialized()
    {
        var filePath = “wwwroot/Sample.xlsx”;
        DataSourceBytes= File.ReadAllBytes(filePath);
    }
}

In this example, the Spreadsheet component loads an Excel file and displays it directly inside the MAUI Blazor application.

Once the application runs, users can interact with the spreadsheet just as they would in a traditional spreadsheet application. Features include:

  • Cell editing
  • Formula calculations
  • Sorting and filtering
  • Import and export support
  • Ribbon-based commands

All of these capabilities run inside a native .NET MAUI app while allowing you to reuse the same Blazor-based UI across multiple platforms.

Excel-like Spreadsheet UI in a .NET MAUI Blazor Hybrid App
Excel-like Spreadsheet UI in a .NET MAUI Blazor Hybrid App

Common issues to watch for

If the Spreadsheet doesn’t appear as expected, here are a few common issues to check:

  • Blank page or missing styles: Verify that the required CSS and JavaScript references have been added correctly to index.html.
  • File not found errors: Make sure the Excel file is located in the wwwroot folder and configured with the appropriate build action.
  • Licensing issues: Ensure that your Syncfusion license key or trial configuration has been set up before running the application.

Checking these items early can help you avoid some of the most common setup issues and make the integration process much smoother.

GitHub reference

Want to see everything in action? Check out our GitHub sample for creating a Syncfusion Blazor Spreadsheet in .NET MAUI.

Frequently Asked Questions

When should I use a spreadsheet UI instead of a DataGrid in .NET MAUI?

Use a spreadsheet UI when users need Excel‑like interactions such as free‑form cell editing, formulas, copy/paste workflows, or file‑based data entry rather than row‑based records.

How does Blazor Hybrid help in building spreadsheet UIs for .NET MAUI?

Blazor Hybrid allows web-based UI components to run inside a native .NET MAUI app using a shared codebase, combining Blazor UI reuse with native performance and platform access.

Can this approach be used across mobile and desktop platforms?

Yes. The same Blazor components run consistently across Android, iOS, Windows, and macOS, while MAUI handles native app hosting and lifecycle management.

What are common issues when hosting a spreadsheet UI in a MAUI Blazor app?

The most common issues are missing CSS or JavaScript references, incorrect Excel file paths, and licensing or initialization issues during app startup.

Syncfusion Blazor components can be transformed into stunning and efficient web apps.

Conclusion

Embedding a spreadsheet UI into a .NET MAUI app using Blazor Hybrid is a practical way to deliver Excel-like data editing across platforms from a single codebase. By combining MAUI’s native capabilities with Blazor’s reusable web components, you can build applications that are easier to maintain, faster to develop, and more familiar to end users.

This approach works especially well for data-heavy business apps where flexibility, consistency, and productivity matter more than simple tabular display.

Additionally, Syncfusion’s Spreadsheet Editor SDK is available across multiple platforms, including  AngularASP.NET CoreJavaScriptASP.NET MVCVueWPFWinForms, and UWP, as well as Document SDK libraries.

If you’re a Syncfusion user, you can download the setup from the license and downloads page. Otherwise, you can download a free 30-day trial.

You can also contact us through our support forumsupport portal, or feedback portal for queries. We are always happy to assist you!

Read the whole story
alvinashcraft
1 hour ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories