CRMSeptember 2026 · 4 min read

CRM System in C#: From Lead to Closing

By go2ismail · Published · .NET 10

At a glance

A lead owns the opportunity: company profile, targeted and closed amounts, a seven-stage pipeline, closing status, contacts, and activities. Campaigns group leads with budgets and expenses; sales teams group the people who pursue them.

Model the business around the lead

In Indotalent’s Blazor CRM Source Code, the Lead entity is the center of the sales process. Every lead inherits BaseEntity, so it carries a string Id, audit timestamps and actors, and an IsDeleted flag. It also implements IHasAutoNumber, which gives each lead a generated document number when it is inserted.

The lead holds both company data and opportunity data in one row. Company fields cover name, description, address, phone, fax, email, website, and a set of social profiles. Opportunity fields include prospecting and closing dates, targeted and closed amounts, and a group of scoring fields. This is a single aggregate rather than a normalized set of tables for each address or social channel, which keeps the create and edit forms compact.

Represent the opportunity with fields and scores

Four nullable decimal scores describe the opportunity: BudgetScore, AuthorityScore, NeedScore, and TimelineScore. They form the classic BANT-style qualification dimension used by the dashboard’s qualification tiles. AmountTargeted records the deal size being pursued and AmountClosed records the amount that was actually won.

Dates carry the lifecycle: DateProspecting records when the lead entered the pipeline, DateClosingEstimation the forecast, and DateClosingActual the day the deal closed. A ClosingNote keeps a free-text reason on the record.

Move through pipeline stages with enums

Stage and status are enums, not free text. PipelineStage defines seven ordered values, from Prospecting through Qualification, NeedAnalysis, Proposal, Negotiation, and DecisionMaking to Closed. Each value carries a [Description] label used by list screens and lookup data, for example "1. Prospecting".

ClosingStatus is separate from stage and holds three values: ClosedLost, ClosedWon, and OnProgress. Closing a deal in the UI is an edit operation: the update form sets PipelineStage to Closed, sets ClosingStatus to ClosedWon or ClosedLost, records AmountClosed and DateClosingActual, and posts the lead update. Dashboard calculations such as won-deal counts, pipeline value, and campaign return use ClosingStatus.ClosedWon and the closed amount.

Attach contacts and activities to a lead

LeadContact represents a person at the target company. It stores full name, a full address block, phone and mobile, email, website, and social profiles, plus an AvatarName used by the avatar upload flow. A contact belongs to exactly one lead through LeadId.

LeadActivity records follow-up work such as a call, email, meeting, or event. It stores Summary, Description, FromDate, ToDate, and a LeadActivityType enum whose values include Phone, Email, Social Media, Meeting, Event, and Other. The lead entity exposes both child collections as navigation properties, and the dashboard reads future activities as upcoming follow-ups.

Run campaigns with budget and expense

Campaign groups related sales activity. It records title, description, TargetRevenueAmount, start and finish dates, a CampaignStatus with values such as Draft, Confirmed, OnProgress, Finished, and Archived, and an owning SalesTeam. Campaign exposes three child collections: LeadList, BudgetList, and ExpenseList.

Budget and Expense are independent entities that reference a campaign. Both carry a title, description, date, amount, and their own status enum, and both implement auto-numbering. Campaign screens manage budgets and expenses as child dialogs, while standalone Budget and Expense screens manage the same entities across all campaigns. There is no automatic remaining-budget calculation; the dashboard computes budget totals and actual expenses separately and derives campaign return from won amounts against expenses.

Organize sales teams and representatives

SalesTeam holds a name and description and owns a collection of SalesRepresentative members. A representative records name, job title, employee number, phone, email, description, and its team reference. A campaign and each lead both reference a sales team, so the same team name appears consistently across the plan and the deals assigned to it. Teams enforce a unique name at creation time, which is the one CRM creation handler in the inspected scope that raises an AlreadyExistsException on duplicates.

Keep customer master data separate

The application also contains a Customer module under Features/Thirdparty/, shared with the wider codebase. Customer and CustomerContact store customer organization and contact data, but there is no foreign key between Lead and Customer and no handler that converts a won lead into a customer. If your workflow expects a win to create a customer record, that conversion is an extension you would add rather than behavior already present.

The model therefore stays readable: leads, their people, their follow-ups, and the campaigns and teams that drive them. CRM Database Design shows how these entities relate, CRM Database Schema lists the actual columns and keys, and Blazor CRM REST API Design shows how the model is exposed over HTTP. See this architecture implemented in a complete Blazor CRM application.