Esports Tournament Manager – Project Plan

Overview

A Blazor Server web application for managing esports tournaments. Organizers can create tournaments, register players and teams, generate brackets, and record match results. The bracket view updates live as results come in.

Tech Stack: Blazor Server · Entity Framework Core (SQLite) · C# 14 · .NET 10

Curriculum Coverage Summary

TopicWhere it appears
OOP – Inheritance & PolymorphismParticipant hierarchy, Tournament hierarchy, virtual methods
InterfacesIHasId, IBracketGenerator, IMatchResult
RecursionBracket generation, winner resolution, Blazor recursive component
Regular ExpressionsGamertag / team-tag / URL validation service
EF Core + Repository PatternAll data access via ARepository<T> base class
LINQFiltering, ordering and projecting in service and repository layers
CollectionsList<T>, Dictionary<K,V>, Queue<T>, HashSet<T> in business logic
Delegates & EventsMatch-result event pipeline, tournament state-change notifications
Extension MethodsFluent helpers on domain types and IEnumerable<T>
C# AttributesData-annotation validation, custom [GameTag] attribute, EF configuration

Milestone 1 – Project Setup & Git Workflow

Goal: A running Blazor Server skeleton committed to a remote repository.

What to implement

  • Create a new Blazor Server project (dotnet new blazorserver -n EsportsManager).
  • Add NuGet packages: Microsoft.EntityFrameworkCore.Sqlite, Microsoft.EntityFrameworkCore.Design.
  • Set up the folder structure:
    EsportsManager/
    ├── Models/          # Domain classes
    ├── Data/            # DbContext, Migrations
    ├── Repositories/    # Abstract base + concrete repositories
    ├── Services/        # Business logic
    ├── Components/      # Reusable Blazor components
    └── Pages/           # Blazor pages
    
  • Initialize a git repository, add a .gitignore (Visual Studio template), create an initial commit, add a remote and push.

Hints

  • Use dotnet new gitignore to generate a standard .gitignore.
  • Keep appsettings.Development.json out of version control (add to .gitignore); store the connection string there.
  • Commit message convention: feat:, fix:, refactor: prefixes keep history readable.

Milestone 2 – Domain Model (OOP, Inheritance, Attributes)

Goal: All domain classes defined with a clean inheritance hierarchy and validation attributes. No database yet – just plain C# classes.

Class hierarchy

Participant  (abstract)
├── Player   – individual competitor
└── Team     – group of Players

Tournament   (abstract)
├── SingleEliminationTournament
└── RoundRobinTournament

Match        – one game between two Participants
BracketNode  – node in the bracket tree (left/right children + Match)

What to implement

Participant (abstract base)

public abstract class Participant
{
    public int Id { get; set; }
    [Required, GameTag]               // custom attribute (see below)
    public string GameTag { get; set; } = string.Empty;
    public abstract string DisplayName { get; }  // polymorphic
}

Player : Participant

  • Properties: FirstName, LastName, Email
  • DisplayName → returns $"{FirstName} {LastName} ({GameTag})"

Team : Participant

  • Properties: TeamName, LogoUrl, List<Player> Members
  • DisplayName → returns TeamName

Tournament (abstract)

  • Properties: Id, Name, Game, StartDate, Status (enum: Draft, Active, Finished)
  • Abstract method GenerateBracket(List<Participant> participants)

SingleEliminationTournament : Tournament – overrides GenerateBracket (used in Milestone 6)

RoundRobinTournament : Tournament – overrides GenerateBracket (used in Milestone 6)

Match

  • Properties: Id, ParticipantA, ParticipantB, WinnerId, PlayedAt
  • Interface: IMatchResult with bool IsDecided { get; } and Participant? GetWinner()

Custom attribute [GameTag]

[AttributeUsage(AttributeTargets.Property)]
public class GameTagAttribute : ValidationAttribute
{
    // validates with Regex in Milestone 7
}

BracketNode

public class BracketNode
{
    public Match? Match { get; set; }
    public BracketNode? Left  { get; set; }   // upper-half winner feeds here
    public BracketNode? Right { get; set; }   // lower-half winner feeds here
}

Hints

  • Circular references and [JsonIgnore]: EF Core encourages bidirectional navigation properties – for example Team has a List<Player> Members, and each Player might have a Team Team property pointing back. When Blazor (or any JSON serializer) tries to serialize such an object it follows Team → Player → Team → Player → ... forever and throws a JsonException. This is called a circular reference. Fix it by adding [JsonIgnore] to the “back-pointer” side (usually the child pointing to the parent), which tells the serializer to skip that property entirely.
  • All data-annotation attributes ([Required], [StringLength], [EmailAddress]) count as curriculum content for C# Attributes.

Milestone 3 – Data Layer (EF Core + Repository Pattern)

Goal: Persist and retrieve all domain objects via the ARepository<T> abstraction.

SQLite vs MySQL

So far you have used MySQL, which runs as a separate server process that your application connects to over a network socket. SQLite is different: it stores the entire database in a single .db file that lives inside the project folder. There is no server to install or start – EF Core reads and writes the file directly. For a classroom project this is ideal (zero setup, works on any machine after a git clone). The EF Core API, migrations, and LINQ queries are identical between SQLite and MySQL, so switching later is just a one-line config change.

What to implement

IHasId interface – required by the generic base repository

public interface IHasId
{
    int Id { get; }
}

All domain entities (Player, Team, Tournament, Match) must implement this interface.

ARepository<TEntity> abstract base class

public abstract class ARepository<TEntity> where TEntity : class, IHasId
{
    protected readonly AppDbContext DbContext;
    protected readonly DbSet<TEntity> Table;
 
    protected ARepository(AppDbContext dbContext)
    {
        DbContext = dbContext;
        Table = dbContext.Set<TEntity>();
    }
 
    public TEntity? GetById(int id)
        => Table.FirstOrDefault(r => r.Id == id);
 
    public IEnumerable<TEntity> GetAll()
        => Table.ToList();
 
    public IEnumerable<TEntity> Get(Expression<Func<TEntity, bool>> filter)
        => Table.Where(filter).ToList();
 
    public TEntity Add(TEntity entity)
    {
        Table.Add(entity);
        DbContext.SaveChanges();
        return entity;
    }
 
    public TEntity Update(TEntity entity)
    {
        Table.Update(entity);
        DbContext.SaveChanges();
        return entity;
    }
 
    public bool Delete(int id)
    {
        var deleted = Table.Where(e => e.Id == id).ExecuteDelete();
        return deleted == 1;
    }
}

AppDbContext : DbContext

  • DbSet<Player>, DbSet<Team>, DbSet<Tournament>, DbSet<Match>
  • Configure TPH (Table Per Hierarchy) for Participant and Tournament with a discriminator column.
  • Seed a few demo players/tournaments in OnModelCreating.

Concrete repositories – each is just a constructor pass-through; add entity-specific query methods as needed.

public class PlayerRepository : ARepository<Player>
{
    public PlayerRepository(AppDbContext dbContext) : base(dbContext) { }
}
 
public class TeamRepository : ARepository<Team>
{
    public TeamRepository(AppDbContext dbContext) : base(dbContext) { }
 
    // Example of an entity-specific method built on top of the base:
    public Team? GetWithMembers(int id)
        => Table.Include(t => t.Members).FirstOrDefault(t => t.Id == id);
}
 
public class TournamentRepository : ARepository<Tournament>
{
    public TournamentRepository(AppDbContext dbContext) : base(dbContext) { }
}
 
public class MatchRepository : ARepository<Match>
{
    public MatchRepository(AppDbContext dbContext) : base(dbContext) { }
}

Register everything in Program.cs

builder.Services.AddDbContext<AppDbContext>(...);
builder.Services.AddScoped<PlayerRepository>();
builder.Services.AddScoped<TeamRepository>();
builder.Services.AddScoped<TournamentRepository>();
builder.Services.AddScoped<MatchRepository>();

Hints

  • Run dotnet ef migrations add InitialCreate and dotnet ef database update.
  • For TPH, use modelBuilder.Entity<Participant>().HasDiscriminator<string>("Type").
  • Keep repositories thin – no business logic, only data access.

Milestone 4 – Service Layer (Collections & LINQ)

Goal: A TournamentService and PlayerService that implement business logic using the most important C# collection types and LINQ.

Collections to use intentionally

TypeWhere
List<Participant>Ordered list of tournament registrations
Dictionary<int, Match>Fast match lookup by id in bracket processing
Queue<Match>Scheduled match queue (next matches to be played)
HashSet<int>Track which participant IDs are already registered (uniqueness)

What to implement

TournamentService

// Returns tournaments ordered by start date, filtered by game name
IEnumerable<Tournament> GetUpcomingByGame(string game);
 
// Returns top N players by win count (simple LINQ, no grouping)
IEnumerable<Player> GetTopPlayers(int n);
 
// Register a participant – uses HashSet to prevent duplicates
bool Register(int tournamentId, Participant participant);
 
// Build the scheduled match queue from the bracket
Queue<Match> BuildMatchQueue(BracketNode root);

PlayerService

// Search players by partial GameTag match (LINQ Where + Contains)
IEnumerable<Player> Search(string query);
 
// Select only Id + DisplayName for a dropdown (LINQ Select projection)
IEnumerable<(int Id, string Label)> GetDropdownItems();

LINQ guidelines for students

  • Use method syntax (Where, Select, OrderBy, FirstOrDefault, Count, Any).
  • No join, group by or complex aggregations – keep it readable.
  • Prefer IEnumerable<T> as return type from services; let callers materialize with .ToList() when needed.

Hints

  • Inject repositories into services via constructor injection.
  • The Queue<Match> is a great conversation starter: why a Queue and not a List here?

Milestone 5 – Delegates, Events & Extension Methods

Goal: A reactive event system so that UI components can react to match results and tournament state changes without tight coupling.

What to implement

EventArgs classes

public class MatchResultEventArgs : EventArgs
{
    public required Match Match { get; init; }
    public required Participant Winner { get; init; }
}
 
public class TournamentStateEventArgs : EventArgs
{
    public required Tournament Tournament { get; init; }
    public required TournamentStatus NewStatus { get; init; }
}

TournamentEventService – uses the standard EventHandler<TEventArgs> delegate

public class TournamentEventService
{
    public event EventHandler<MatchResultEventArgs>?     OnMatchResultRecorded;
    public event EventHandler<TournamentStateEventArgs>? OnTournamentStateChanged;
 
    public void RecordResult(Match match, Participant winner)
    {
        // persist result via repository ...
        OnMatchResultRecorded?.Invoke(this, new MatchResultEventArgs
        {
            Match  = match,
            Winner = winner
        });
    }
 
    public void ChangeState(Tournament t, TournamentStatus status)
    {
        // persist state change via repository ...
        OnTournamentStateChanged?.Invoke(this, new TournamentStateEventArgs
        {
            Tournament = t,
            NewStatus  = status
        });
    }
}
  • Raise the events after persisting changes to the database.
  • Blazor components subscribe in OnInitialized and unsubscribe in Dispose.
  • Call InvokeAsync(StateHasChanged) in the handler to trigger a re-render.

IDisposable on a Blazor component

Students already know the IDisposable pattern from plain C#. In Blazor the only new part is the @implements directive at the top of the .razor file – Blazor’s component lifecycle then calls Dispose() automatically when the component is removed from the page.

@implements IDisposable
 
@code {
    [Inject] TournamentEventService EventService { get; set; } = default!;
 
    protected override void OnInitialized()
    {
        // Subscribe when the component is first rendered
        EventService.OnMatchResultRecorded += HandleMatchResult;
    }
 
    private void HandleMatchResult(object? sender, MatchResultEventArgs e)
    {
        // e.Match and e.Winner are available here
        InvokeAsync(StateHasChanged); // triggers a re-render on the UI thread
    }
 
    public void Dispose()
    {
        // Unsubscribe to prevent memory leaks
        EventService.OnMatchResultRecorded -= HandleMatchResult;
    }
}

Extension Methods (put in Extensions/ folder)

// On IEnumerable<Match>
public static IEnumerable<Match> Decided(this IEnumerable<Match> matches)
    => matches.Where(m => m.IsDecided);
 
public static IEnumerable<Match> Pending(this IEnumerable<Match> matches)
    => matches.Where(m => !m.IsDecided);
 
// On Participant
public static bool IsTeam(this Participant p) => p is Team;
 
// On string – preview of Regex work in Milestone 7
public static bool IsValidGameTag(this string s)
    => Regex.IsMatch(s, @"^[A-Za-z0-9_]{3,16}$");

Hints

  • TournamentEventService must be registered as AddSingleton so all Blazor circuits share the same event stream.
  • Forgetting to unsubscribe events is a classic memory-leak bug – the IDisposable pattern above is the standard fix.

Milestone 6 – Bracket Engine (Recursion)

Goal: Implement single-elimination bracket generation and winner resolution using recursion, and render the bracket as a recursive Blazor component.

What to implement

BracketService

// Recursively build a bracket tree from a list of participants.
// Base case: 1 participant → leaf node with no match (bye).
// Recursive case: split list in half, recurse on each half,
//                 create a match node whose children are the two sub-roots.
public BracketNode GenerateSingleElimination(List<Participant> participants)
 
// Recursively resolve who has won up to this node.
// Returns null if any match below is undecided.
public Participant? ResolveWinner(BracketNode node)
 
// Recursively collect all Match objects in the tree (pre-order).
public List<Match> CollectMatches(BracketNode node)

Recursive Blazor component BracketNodeView.razor

@* Renders a BracketNode and then recursively renders its children *@
<div class="bracket-node">
    @if (Node.Match is not null)
    {
        <MatchCard Match="Node.Match" />
    }
    <div class="bracket-children">
        @if (Node.Left is not null)  { <BracketNodeView Node="Node.Left"  /> }
        @if (Node.Right is not null) { <BracketNodeView Node="Node.Right" /> }
    </div>
</div>

Hints

  • Pad the participant list to the next power of two with null (bye slots) before generating so the bracket is always balanced.
  • When saving a generated bracket to the database, use CollectMatches to get the flat list and insert all Match objects.
  • Store the BracketNode tree in memory (or serialize to JSON in a column) for fast rendering without re-querying.

Milestone 7 – Input Validation (Regular Expressions)

Goal: A central ValidationService using Regex that is shared between the custom attribute from Milestone 2 and the Blazor forms.

Patterns to implement

public static class ValidationPatterns
{
    // Gamertag: 3–16 chars, letters, digits, underscores
    public const string GameTag = @"^[A-Za-z0-9_]{3,16}$";
 
    // Team tag: 2–5 uppercase letters (e.g. "TSM", "NaVI")
    public const string TeamTag = @"^[A-Z]{2,5}$";
 
    // Tournament name: letters, digits, spaces, hyphens, 4–60 chars
    public const string TournamentName = @"^[A-Za-z0-9 \-]{4,60}$";
 
    // Twitch/YouTube stream URL (optional field)
    public const string StreamUrl = @"^https://(www\.)?(twitch\.tv|youtube\.com)/\S+$";
}

ValidationService

public class ValidationService
{
    public bool IsValidGameTag(string input)
        => Regex.IsMatch(input, ValidationPatterns.GameTag);
 
    public string SanitizeGameTag(string input)
        => Regex.Replace(input.Trim(), @"[^A-Za-z0-9_]", "_");
 
    public IEnumerable<string> ExtractMentions(string text)
        => Regex.Matches(text, @"@([A-Za-z0-9_]{3,16})")
                .Select(m => m.Groups[1].Value);
}

Wire up [GameTag] attribute

  • GameTagAttribute.IsValid should use ValidationService (or call Regex.IsMatch directly) to validate the property value.

Blazor form integration

  • Use <DataAnnotationsValidator /> in forms – the custom attribute fires automatically.
  • Add a real-time feedback label that calls ValidationService on @oninput.

Hints

  • Compile frequently used patterns with Regex.IsMatch(..., ..., RegexOptions.Compiled) for a small performance gain – worth discussing with students.
  • Regex.Replace for sanitization is a great example of regex beyond just validation.

Milestone 8 – Blazor UI: Management Pages

Goal: Full CRUD user interface for Players, Teams, and Tournaments.

Pages to build

PageRouteFeatures
Players/Index/playersSearchable list (calls PlayerService.Search), link to detail
Players/Edit/players/{id}Create/edit form with [DataAnnotations] validation
Teams/Index/teamsList with member count
Teams/Edit/teams/{id}Form + add/remove members from List<Player>
Tournaments/Index/tournamentsFilter by status using LINQ, sort by date
Tournaments/Edit/tournaments/{id}Create/edit, participant registration panel

Component guidelines

  • Extract reusable components: <ParticipantCard>, <StatusBadge>, <ConfirmDialog>.
  • Use Blazor’s built-in EditForm with DataAnnotationsValidator and ValidationSummary.
  • Subscribe to TournamentEventService events where real-time feedback is needed (remember: @implements IDisposable).

Hints

  • Use NavigationManager.NavigateTo(...) after a successful save.
  • Keep page components thin – call services, don’t write business logic inline.
  • @inject the required services at the top of each page.

Milestone 9 – Blazor UI: Tournament Bracket View

Goal: A visual, live-updating bracket page that uses the recursive component from Milestone 6 and reacts to events from Milestone 5.

What to implement

Tournaments/Bracket.razor (/tournaments/{id}/bracket)

  • Load the bracket tree via BracketService.
  • Render it with <BracketNodeView Node="rootNode" />.
  • Subscribe to TournamentEventService.OnMatchResultRecorded – re-resolve the winner and call StateHasChanged on each update (use @implements IDisposable to clean up).

<MatchCard> component

  • Shows ParticipantA vs ParticipantB.
  • If the match is pending and the user is an organizer, show a “Record Result” button.
  • Clicking opens a modal where the winner is selected → calls TournamentEventService.RecordResult.

Basic CSS bracket layout

  • Use flexbox or CSS grid to display the tree horizontally (rounds as columns).
  • Lines between nodes can be drawn with CSS borders/pseudo-elements.

Hints

  • The recursive Blazor component naturally mirrors the recursive data structure – highlight this connection to students.
  • Blazor Server uses SignalR under the hood, so live updates across browser tabs work out of the box when events are raised on the singleton TournamentEventService.

Milestone 10 – Final Polish & Deployment

Goal: A complete, presentable application with error handling, a seeded demo dataset, and a clean final commit.

What to implement

  • Exception handling: Wrap service calls in try/catch in pages; show user-friendly error toasts (a simple ToastService with a delegate/event is a nice callback to M5).
  • Seed data: Expand the EF Core seed to include 2 full tournaments with players, teams, and a partially played bracket so the app looks good on first run.
  • README.md: Short description, setup steps (dotnet ef database update, dotnet run), and a screenshot of the bracket view.
  • Final git tag: git tag v1.0 after the last commit.

Checklist before final push

  • All compiler warnings resolved
  • No hardcoded connection strings in committed files
  • dotnet ef migrations are up to date
  • App runs from a clean clone with only dotnet run

Milestone Overview

#NameKey Curriculum Topics
1Project Setup & GitGit, project structure
2Domain ModelOOP, inheritance, polymorphism, interfaces, attributes
3Data LayerEF Core, SQLite, ARepository<T>, migrations
4Service LayerCollections (List, Dictionary, Queue, HashSet), LINQ
5Delegates, Events & ExtensionsEventHandler<T>, extension methods, IDisposable in Blazor
6Bracket EngineRecursion, recursive Blazor component
7ValidationRegular expressions, custom attributes
8Management UIBlazor forms, components, data binding
9Bracket ViewRecursive component, live events, Blazor Server/SignalR
10Final PolishError handling, seeding, documentation