🛠 Series: ERP RFQ ModulePart 8 of 8

"Fluent API keeps your domain entities clean. Data annotations are a shortcut that clutters the code you read most."

This is the final post in the ERP RFQ Module series. We configure EF Core 8 with Fluent API, run the InitialCreate migration, and seed realistic Saudi construction data so the project demo works out of the box. We also connect Power BI DirectQuery for live analytics.

Part of the Green Concrete ERP project. View full project page →

Fluent API vs Data Annotations

Data annotations ([Required], [MaxLength(20)], [Column("Status")]) go on the entity class — in the Domain layer. This means the Domain layer has a reference to EntityFrameworkCore, breaking the clean architecture dependency rule. Fluent API keeps configuration in the Infrastructure layer where it belongs, leaving Domain entities as pure C# classes.

// Domain entity — clean, no EF annotations
public class Quotation
{
    public Guid Id { get; set; }
    public string QuotationNumber { get; set; } = string.Empty;
    public decimal TotalAmount { get; set; }
    public QuotationStatus Status { get; set; }
    public Project Project { get; set; } = null!;
    public Guid ProjectId { get; set; }
}

// Infrastructure — Fluent API configuration
builder.Property(q => q.QuotationNumber).IsRequired().HasMaxLength(20);
builder.Property(q => q.TotalAmount).HasPrecision(18, 4);
builder.HasConversion(
    q => q.Status.ToString(),
    s => Enum.Parse<QuotationStatus>(s));

Migrations + Seed Data Commands

# Run from solution root
cd GreenConcrete.Infrastructure
dotnet ef migrations add InitialCreate \
    --startup-project ../GreenConcrete.WebApi
dotnet ef database update \
    --startup-project ../GreenConcrete.WebApi

# If you get "already exists" on re-run:
dotnet ef database drop --startup-project ../GreenConcrete.WebApi --force
dotnet ef database update --startup-project ../GreenConcrete.WebApi

Seed data is applied in DbContext.OnModelCreating via HasData(), which runs as part of every migration:

// 3 users: ahmed (Salesperson), khalid (Manager), admin (Admin)
// 15 Saudi construction companies: ARAMCO Construction,
//   SABIC Projects, Al-Rajhi Contracting, Nesma & Partners...
// 25 projects across Riyadh, Jeddah, Dammam
// 30 quotations in Draft, PendingApproval, Approved, Rejected states
// All amounts in SAR (Saudi Riyal)

Power BI DirectQuery Connection

// In Power BI Desktop: Get Data → SQL Server
// Server: localhost\SQLEXPRESS (or your SQL Server name)
// Database: GreenConcreteERP
// Data Connectivity: DirectQuery

// Key tables for analytics:
// Quotations — status, amount, dates
// Projects — client, location
// MaterialRows — mixture details, OPC percentages
// Users — salesperson performance

// The HasConversion (Status as string) makes status readable
// in Power BI without joining a lookup table:
// Quotations[Status] = "Approved" (not 2 or some integer)

🎯 Quick Check

Q1: Why does storing QuotationStatus as string (instead of int) help in Power BI?

Show Answer

When stored as int, Power BI shows 0, 1, 2, 3 — meaningless without a lookup table. When stored as string ("Draft", "Approved", etc.), the value is self-documenting in every report and query. You can write DAX measures like CALCULATE(SUM(TotalAmount), Quotations[Status] = "Approved") without any joins. The slight storage overhead (string vs int) is irrelevant for ERP data volumes.

Q2: Why use the --startup-project flag with dotnet ef commands?

Show Answer

EF Core tools need to instantiate your DbContext, which requires a dependency injection container and configuration (connection string). The DbContext is in GreenConcrete.Infrastructure (no startup code). GreenConcrete.WebApi has Program.cs which registers all services and reads appsettings.json. The --startup-project flag tells EF tools to use WebApi's startup code to build the DI container and get the DbContext configuration.

Q3: What is the risk of using HasData() for seed data in production?

Show Answer

HasData() runs on every migration that references the seeded entities. If you modify seed data (change a company name, add a user), EF Core generates a migration that deletes and re-inserts all seed records for that entity type. In production, this would delete real data. For production systems, use a separate seeder class that checks if data exists before inserting, rather than HasData().

Key Takeaways

  • Fluent API keeps Domain entities clean — no EF Core references in the innermost layer
  • HasConversion stores enums as strings — readable in Power BI DirectQuery without lookup tables
  • --startup-project is required because EF tools need the DI container to resolve DbContext
  • HasData() is fine for demo seed data; use a separate seeder class for production
  • All 8 parts of this series are buildable in 2-3 days — the complete source is on the ERP project page