This is Free Planning Poker, a free tool for software teams to do "planning poker" exercises to estimate the difficulty and length of development tasks. You can probably use it for other purposes if you need, too. It's always going to be free, without limits.
Note that I've just started development on this so some documentation and whatnot is a WIP as this gets set up!
The ideal scenario is that you can "clone and go" without much (if any) work, but there's a couple steps you need right now:
- Fork and clone this repo
- Download and install the .NET 8 SDK
- I recommend using VSCode with the C# Dev Kit
You should be good to go now - hit F5 and watch it run! By default it will use an in-memory store to keep state. This store is not thread safe; in order to get thread safety (and to allow SignalR to use a backplane) you'll need to provide a connection string for Redis. However, the in-memory store is fast and ideal for local debugging scenarios.
- You will need access to some deployment of Redis. Redis' Quick Start docs can help you here.
- Add your Redis connection string in appsettings:
"ConnectionStrings": {
"Redis": "<your-connection-string>"
}
The application will see your connection string and use the Redis store instead of the in-memory store, and it will use Redis as a backplane for SignalR.
In future I want to look into Docker environments to be able to remove standing up your own Redis as being a burden.
You can deploy this project yourself without much fuss. I recommend using Railway, my favorite cloud provider for simple apps (heck, even some complicated scenarios are probably fine here).
In future I want to add some documentation around deploying on Docker, and since this is a .NET app I could include Azure Services documentation easily.
(See also my guide on deploying ASP and Blazor apps on Railway)
- Fork and clone this repo
- Create an account at Railway
- Create a new project, and add a Redis instance to it
- Add a new service from your cloned GitHub repo (Railway will handle building and all)
- Under the Settings for this service, use the following as your Custom Start Command for Deploy:
./out/PlanningPoker.Server
- Add your Redis connection string as an environment variable:
ConnectionStrings__Redis
(Use Railway's reference variables to make this easy) - Add two environment variables required for .NET:
CONTENT_ROOT_PATH=./
NIXPACKS_CSHARP_SDK_VERSION=8.0
Now you should be good to go! Railway can provide a domain name for your instance of FreePlanningPoker so you can use it.
Note that while you technically can deploy this without Redis, I don't recommend it since the in-memory store is not thread safe. If you want to make it thread safe I'd be more than happy to entertain that PR!
In future I'll be adding some of these settings to a Railway config file in the repo, eliminating the need for a couple of these steps.
This section TBD.
This is a .NET 8 app, and as such you should be able to publish it as a dockerfile with dotnet publish
. I have yet to get this working. This would be a good first issue if you want to contribute by adding documentation for this!
This section TBD.
If you're hoping to contribute, this would be a good first issue to add documentation for this! Realistically, if you have an Azure subscription you should be able to click the Publish button in Visual Studio and send it up in a new App Service.
The client is a Blazor WASM SPA, the server is ASP and they communicate exclusively over SignalR (websockets). The server uses Redis as a backplane for SignalR and to store active sessions - this allows the server to scale horizontally.
The SignalR communication is defined by two interfaces in the PlanningPoker
project: ISessionHub defines client-to-server communication (some of which does require a round trip) and ISessionHubClient defines server-to-client communication (none of which requires a round trip; this must be asynchronous communication).
The logic for the server methods is in SessionHub in the Server
project. This class contains the very minimal business rules and the scheme of notifying clients of changes through ISessionHubClient
. Clients are grouped by session id, and only clients in a session will receive notifications for it. One future goal is to separate the DAL from this class so that a separate in-memory DAL can be implemented for easier local debugging.
State is kept by one of the two classes implementing IStore: either InMemoryStore or RedisStore. The former is used for local debugging scenarios where Redis isn't strictly needed, while the latter is used for production deployments and any networking-related debugging and testing.
If you are adding a method on the server for the client to call, you'll update ISessionHub
, implement the server logic in SessionHub
and the store classes, then you'll update the client's SessionState
to call it (see below). If you're adding a method on the client to call, you'll update ISessionHubClient
, implement the client logic in SessionState
(see below), then you'll update the server's SessionHub
to call down through that method. Everything is strongly-typed by these interfaces on both the client and server, keeping you from needing to using magic strings.
Configuration and dependency injection are all set up in Program; there's really not a lot there.
Session data is stored in Redis across several keys to eliminate or minimize race conditions. The keys and their values are:
{sessionId}
(guid): Hash with values "Title" and "State".{sessionId}:participants
: List with values being the IDs of the participants in the session.{sessionId}:participants:{participantId}
: Hash with values "Name", "Points", and "Stars".
All entries associated with a session are removed from Redis when the last participant leaves the session.
There's two main files to care about: SessionState and SessionPage. The SessionState
class keeps the state for the user and their session, and handles commands from the UI and notifications from the server which mutate state. As such, it also maintains the SignalR connection and the navigation in the app (this is trivial, that's just moving from the homepage to the session on creation). When the state mutates, the OnStateChanged
event is raised.
SessionState
implements ISessionHubClient
and keeps an instance of ISessionHub
, which fulfil the SignalR communication requirements. These are set up in EnsureInitialized
, and torn down in LeaveAsync
. Note that EnsureInitialized
uses some fancy source generation - the package for this is from Microsoft, though it's undocumented and hasn't been updated in two years. If you dig enough online, you'll find Kristoffer Strube's post about them. When adding server functionality, this is the only file you need to change unles the functionality you're adding requires new UI components.
SessionPage
is the user interface for almost the entire application. The user will first create a session on the homepage (Index.razor
) but then all the work in the session is done on this page. This page listens to the OnStateChanged
event from the state and calls StateHasChanged
on itself when it receives that event. Several components for the UI are broken out into separate Razor components in the Components directory.
Dark/light color mode is handled through JS, as is E2E encryption; the code for these functions can be found in index.html. I wrote a blog post on E2E encryption which covers the implementation used here.
Please do! I think the above gives a fair quick overview of the project structure and how to add some features. I've got several good first issues and I'm always happy to discuss suggestions for what to include, modify, etc.
If you would like to champion an issue, please leave a comment saying you'd like to - I'll assign the issue to you and I'll be happy to clarify any questions.
I don't have formal code standards on this proejct yet; it's quite small and young. I ask that your code be kept minimal, tidy, and in-keeping with the code that's already here. In future as the application solidifies then a more defined coding and architectural standard will probably emerge - I find that a codebase will generally reveal its own standards over time and I prefer allowing that process rather than imposing a (probably wrong) idea on the codebase from the start.