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
| Topic | Where it appears |
|---|---|
| OOP – Inheritance & Polymorphism | Participant hierarchy, Tournament hierarchy, virtual methods |
| Interfaces | IHasId, IBracketGenerator, IMatchResult |
| Recursion | Bracket generation, winner resolution, Blazor recursive component |
| Regular Expressions | Gamertag / team-tag / URL validation service |
| EF Core + Repository Pattern | All data access via ARepository<T> base class |
| LINQ | Filtering, ordering and projecting in service and repository layers |
| Collections | List<T>, Dictionary<K,V>, Queue<T>, HashSet<T> in business logic |
| Delegates & Events | Match-result event pipeline, tournament state-change notifications |
| Extension Methods | Fluent helpers on domain types and IEnumerable<T> |
| C# Attributes | Data-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 gitignoreto generate a standard.gitignore. - Keep
appsettings.Development.jsonout 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→ returnsTeamName
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:
IMatchResultwithbool IsDecided { get; }andParticipant? 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 exampleTeamhas aList<Player> Members, and eachPlayermight have aTeam Teamproperty pointing back. When Blazor (or any JSON serializer) tries to serialize such an object it followsTeam → Player → Team → Player → ...forever and throws aJsonException. 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
ParticipantandTournamentwith 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 InitialCreateanddotnet 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
| Type | Where |
|---|---|
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 byor 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
OnInitializedand unsubscribe inDispose. - 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
TournamentEventServicemust be registered asAddSingletonso all Blazor circuits share the same event stream.- Forgetting to unsubscribe events is a classic memory-leak bug – the
IDisposablepattern 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
CollectMatchesto get the flat list and insert allMatchobjects. - Store the
BracketNodetree 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.IsValidshould useValidationService(or callRegex.IsMatchdirectly) 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
ValidationServiceon@oninput.
Hints
- Compile frequently used patterns with
Regex.IsMatch(..., ..., RegexOptions.Compiled)for a small performance gain – worth discussing with students. Regex.Replacefor 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
| Page | Route | Features |
|---|---|---|
Players/Index | /players | Searchable list (calls PlayerService.Search), link to detail |
Players/Edit | /players/{id} | Create/edit form with [DataAnnotations] validation |
Teams/Index | /teams | List with member count |
Teams/Edit | /teams/{id} | Form + add/remove members from List<Player> |
Tournaments/Index | /tournaments | Filter 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
EditFormwithDataAnnotationsValidatorandValidationSummary. - Subscribe to
TournamentEventServiceevents 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.
@injectthe 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 callStateHasChangedon each update (use@implements IDisposableto clean up).
<MatchCard> component
- Shows
ParticipantAvsParticipantB. - 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/catchin pages; show user-friendly error toasts (a simpleToastServicewith 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.0after the last commit.
Checklist before final push
- All compiler warnings resolved
- No hardcoded connection strings in committed files
-
dotnet ef migrationsare up to date - App runs from a clean clone with only
dotnet run
Milestone Overview
| # | Name | Key Curriculum Topics |
|---|---|---|
| 1 | Project Setup & Git | Git, project structure |
| 2 | Domain Model | OOP, inheritance, polymorphism, interfaces, attributes |
| 3 | Data Layer | EF Core, SQLite, ARepository<T>, migrations |
| 4 | Service Layer | Collections (List, Dictionary, Queue, HashSet), LINQ |
| 5 | Delegates, Events & Extensions | EventHandler<T>, extension methods, IDisposable in Blazor |
| 6 | Bracket Engine | Recursion, recursive Blazor component |
| 7 | Validation | Regular expressions, custom attributes |
| 8 | Management UI | Blazor forms, components, data binding |
| 9 | Bracket View | Recursive component, live events, Blazor Server/SignalR |
| 10 | Final Polish | Error handling, seeding, documentation |