cowork-server
cowork-server is a C# API for a coworking space with shared meeting rooms. The interesting part is not the room CRUD. The booking flow has to deal with a shared resource, user permissions, time limits, conflicts, and pricing without putting all of that logic into the API controllers.
Problem
Booking a room is more than checking whether a time slot is empty. A member can reserve only an active room, during its opening hours and inside the booking window, without colliding with another reservation or exceeding the daily limit.
Architecture
The codebase is split into Web API, Application, Domain, Infrastructure, and Framework projects. Controllers send requests through MediatR. Application handlers coordinate repositories and apply booking rules; EF Core persistence stays in infrastructure.
Authentication & Authorization
ASP.NET Core Identity manages users, logins, refresh tokens, OTP codes, roles, and claims. Meeting-room endpoints require an authenticated, active user. Booking commands take the user ID from the current authenticated context, not from the request body.
Booking Domain
A booking records its room, owner, date and time range, status, the room’s hourly price at booking time, billable minutes, and the total amount. The model also stores a monthly allowance record per user, with used and remaining minutes.
Business rules
- No overlapping reservations: a new booking cannot overlap another non-cancelled booking for the same room and date.
- Daily reservation limit: a user’s non-cancelled billable minutes for the day, including the requested duration, cannot exceed the configured maximum.
- Booking window: bookings must be for today through 14 days ahead.
- Room opening hours: the requested interval must fall inside the room’s configured daily start and end times.
- Duration: a booking must be longer than 30 minutes and shorter than the room’s configured maximum duration.
- Monthly allowance: used and remaining minutes are persisted per user and month. The allowance model exists, but it is not yet part of the booking enforcement path.
- Billable minutes and amount: the booking duration becomes billable minutes; the amount uses the room’s hourly price captured at booking time.
CQRS
Commands and queries are separate record types with their own handlers. Commands go through the write path and change state; queries use read repositories and return the data needed by the caller. This is an application-level split, not a separate physical read store.
Why minimal CQRS?
Booking is a business operation with rules, not a generic update to a room schedule. BookMeetingRoom gives the application one place to validate the request, identify the current user, reject conflicts, calculate the amount, and persist the result. Queries can return room availability or a member’s booking history without rebuilding that workflow.
Commands and queries stay inside the same API and use the same database. I wanted the separation in the application code without introducing separate services, databases, or messaging just for the sake of CQRS.
Consistency & Transactions
The booking handler uses an in-process semaphore keyed by room and date before it checks for overlap and saves the booking. This works only within a single application process. If the API were running on multiple instances, the booking conflict would need to be enforced at the database level or handled with a distributed lock.
The codebase uses a shared EF Core unit of work for persistence. It also has a MediatR transaction behavior for commands that opt into the transactional-command interface. The current booking command persists its changes through the shared unit of work directly.
Notifications
The codebase includes a database-backed outbox. A hosted worker reads pending events and routes notification or record events to processors. Notification models support email, in-app, and SMS channels. The booking handler itself does not publish a notification event.
Infrastructure
Infrastructure contains the EF Core repositories, ASP.NET Core Identity stores, caching support, structured logging, and configuration through options. The Web API is the application boundary; controllers are thin request-to-handler adapters.
Testing
The test suite checks that every command and query is a record and has a matching handler. These tests prevent the command/query structure from drifting as new handlers are added.
Technical Decisions
- Kept CQRS in one deployed application and one database instead of adding a broker or separate read store.
- Serialized booking attempts for the same room and date within one process before checking for conflicts.
- Snapshotted the hourly price on the booking so later room-price changes do not change historical amounts.
- Kept monthly allowance state in the model without presenting it as an enforced rule.
- Used an outbox worker for asynchronous notifications instead of sending them from request handlers.