A production-style ASP.NET Web API that exposes an internal corporate employee directory.
A single HTTP request flows through seven distinct layers. Each layer communicates only with the one directly below it, via an interface contract.
flowchart TD
Client(HTTP Client)
subgraph RestWebAPI["RestWebAPI (Presentation Layer)"]
Filters["Global Filters\n(ValidateModelState · CustomException · JwtAuthentication)"]
Controller["LoginController\n(ApiController)"]
end
subgraph Business["Business Layer"]
BSI["BusinessServiceInterface\n(ILoginService)"]
BS["BusinessService\n(LoginService)"]
end
subgraph Data["Data Layer"]
DSI["DataServiceInterface\n(ILoginDataService)"]
DS["DataService\n(LoginDataService)"]
end
subgraph DB["Database Layer"]
EF["Entity Framework\n(DbContext)"]
ADO["ADO.NET\n(OdbcConnection)"]
Dapper["Dapper\n(IDbConnection)"]
end
Client -->|POST /api/login/UserLogin| Filters
Filters --> Controller
Controller --> BSI
BSI --> BS
BS --> DSI
DSI --> DS
DS --> EF
DS -.->|alternative| ADO
DS -.->|alternative| Dapper
- Request hits the global filters — model state validated, exceptions caught before the controller runs
LoginControllerreceives the request; depends only onILoginService(never on concrete implementations)LoginServicemaps the business request to a data request and callsILoginDataServiceLoginDataServiceexecutes the query via one of three ORM strategies (EF, Dapper, or ADO.NET)- Results flow back up through static mapper methods (
EmployeeListBOResponse.Create()) at each layer boundary - Controller wraps the result in a uniform
BOResponse<T>and returns it to the client
| Feature | Implementation | Location |
|---|---|---|
| JWT Authentication | Custom AuthorizationFilterAttribute with HMAC-SHA256 |
Filters/JwtAuthenticationAttribute.cs |
| Request Validation | FluentValidation + DataAnnotations (dual strategy) | BusinessModel/LoginBORequest.cs |
| Entity Framework ORM | DbContext.Database.SqlQuery<T> via DbService |
Database/Context.cs, DataService/DbService.cs |
| ADO.NET | OdbcConnection / DbProviderFactory pattern |
Database/Context.cs (Ado static class) |
| Dapper | IDbConnection.Query<T> |
Database/Context.cs (DapperClass), DataService/LoginDataService.cs |
| Dependency Injection | Ninject — bindings wired at startup | RestWebAPI/App_Start/NinjectWebCommon.cs |
| Swagger / OpenAPI | Swashbuckle with XML doc comments | RestWebAPI/App_Start/SwaggerConfig.cs |
| Global Exception Handling | ExceptionFilterAttribute — returns HTTP 500 with sanitized message |
Filters/CustomExceptionFilter.cs |
| Global Model Validation | ActionFilterAttribute — returns HTTP 400 on invalid model |
Filters/ValidateModelStateFilter.cs |
| Convention + Attribute Routing | Both strategies registered | App_Start/WebApiConfig.cs, App_Start/RouteConfig.cs |
POST /api/login/UserLogin
Authenticates a user and returns a signed JWT.
// Request body
{
"username": "jdoe",
"password": "secret",
"udId": "device-uuid"
}
// Response 200 OK
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
// Response 400 Bad Request (FluentValidation)
{ "message": "UserName cannot be empty" }
// Response 404 Not Found (user not in system)GET /api/login/GetEmployeeList?userId={id}
Returns the employee list for the given user context. Requires Bearer token.
Authorization: Bearer <token>
// Response 200 OK
{
"Code": 0,
"Desc": "",
"Data": [
{ "fName": "Joseph", "lName": "Chirshian", "department": "Engineering", "email": "..." },
...
]
}
// Response 401 Unauthorized (missing or invalid token)
{ "Code": -3, "Desc": "Missing Jwt Token" }GET /api/values — returns placeholder values
GET /api/values/{id}
POST / PUT / DELETE /api/values/{id}
These decisions are the ones interviewers tend to ask about. The short answer for each is why this choice rather than what it does.
Splitting into separate assemblies (BusinessService, DataService, etc.) enforces compile-time boundaries — RestWebAPI physically cannot call LoginDataService directly, because it has no project reference to it. This is a stronger guarantee than naming conventions or code review. It also means each layer is independently deployable and independently testable.
ILoginService and ILoginDataService exist for two reasons:
- Ninject can swap implementations without the calling code changing — the
LoginControllerconstructor receivesILoginServiceand has no idea whether the runtime instance isLoginService, a test stub, or a futureCachedLoginService. - Unit tests can mock the seam —
LoginServiceTestsmocksILoginDataServicewith Moq, so the business logic is tested in complete isolation from the database driver.
Each model type owns its own mapping logic as a static factory method — LoginBORequest.Create(boRequest) returns a LoginRequest, EmployeeListBOResponse.Create(list) returns a BOResponse<EmployeeListBOResponse>. This is deliberately different from AutoMapper: the mapping is explicit, compile-time checked, and colocated with the type it produces. No magic, no configuration file.
A uniform envelope for all API responses. Every endpoint returns the same shape — Code, Desc, Data. Clients always know where to look; error handling is consistent across all endpoints; adding a new field to the envelope propagates automatically to every response.
ValidateModelStateFilter and CustomExceptionFilter are registered globally in WebApiConfig.cs, not applied per-controller. This guarantees that no endpoint can be added to the project without getting validation and exception handling for free. A per-controller attribute is easily forgotten; a global registration is not.
The default ClockSkew in System.IdentityModel.Tokens.Jwt is 5 minutes — meaning a 2-minute token actually lives for 7 minutes. Setting it to TimeSpan.Zero makes the expiry time exact. This matters for short-lived tokens (the UserLogin endpoint issues a 2-minute token) and shows an understanding of token validation internals rather than accepting framework defaults blindly.
The three implementations (Entity Framework, Dapper, ADO.NET) are all wired to the same ILoginDataService contract. You can swap the ORM used for any query by changing a single Ninject binding in NinjectWebCommon.cs — the business layer and API layer are completely unaware of the change. See docs/ORM_COMPARISON.md for when to choose each.
RESTWebAPI.sln
│
├── RestWebAPI/ # Presentation layer — controllers, filters, startup
│ ├── Controllers/
│ │ ├── LoginController.cs # Auth + employee endpoints
│ │ ├── TokenController.cs # JWT generation and validation
│ │ └── ValuesController.cs # Scaffolded CRUD placeholder
│ ├── Filters/
│ │ ├── JwtAuthenticationAttribute.cs # Bearer token validation
│ │ ├── CustomExceptionFilter.cs # Global HTTP 500 handler
│ │ └── ValidateModelStateFilter.cs # Global HTTP 400 handler
│ └── App_Start/
│ ├── WebApiConfig.cs # Routes, global filters, FluentValidation registration
│ ├── NinjectWebCommon.cs # DI container — interface→implementation bindings
│ └── SwaggerConfig.cs # Swashbuckle / OpenAPI configuration
│
├── BusinessServiceInterface/ # Contracts the presentation layer depends on
│ └── ILoginService.cs
│
├── BusinessService/ # Business logic — orchestrates data calls, applies rules
│ └── LoginService.cs
│
├── BusinessModel/ # Request/response models for the business layer
│ ├── BOResponse.cs # Generic list envelope
│ ├── BOResponseSingle.cs # Generic single-item envelope
│ ├── LoginBORequest.cs # Includes FluentValidation LoginValidator
│ ├── UserLoginInfoBOResponse.cs
│ └── EmployeeListBOResponse.cs # Includes static mapper + BOResponse factory
│
├── DataServiceInterface/ # Contracts the business layer depends on
│ └── ILoginDataService.cs
│
├── DataService/ # Data access — EF / Dapper / ADO.NET implementations
│ ├── LoginDataService.cs # Three ORM strategies (EF active; Dapper/ADO commented)
│ └── DbService.cs # EF query wrapper
│
├── DataModel/ # POCO models that match database schema
│ ├── EmployeeMaster.cs
│ ├── LoginRequest.cs
│ └── UserLoginInfoResponse.cs
│
├── Database/ # Database infrastructure
│ └── Context.cs # EF DbContext + ADO.NET Ado class + Dapper DapperClass
│
└── RestWebAPI.Tests/ # Unit tests — NUnit 3 + Moq
└── LoginServiceTests.cs
- Visual Studio 2019 or later (with .NET Framework 4.6.1 workload)
- Windows (IIS Express required for
System.Webhosting)
-
Clone the repo
git clone https://github.com/shwetaptl/RESTWebAPI.git cd RESTWebAPI -
Open the solution
Open RestWebAPI.sln in Visual Studio -
Restore NuGet packages
Tools → NuGet Package Manager → Restore(The
packages/folder is excluded from source control — NuGet restores it on build.) -
Configure database (optional) The data layer currently returns static mock data — the API runs and returns responses without a database connection. To connect a real database, update
Web.config:<connectionStrings> <add name="Entities" connectionString="your-connection-string" providerName="..." /> </connectionStrings> <appSettings> <add key="DataProvider" value="System.Data.Odbc" /> </appSettings>
Then uncomment the relevant query block in
DataService/LoginDataService.cs. -
Run Press
F5or setRestWebAPIas startup project and run. -
Swagger UI
http://localhost:{port}/swagger
# 1. Authenticate — returns a JWT
curl -X POST http://localhost:{port}/api/login/UserLogin \
-H "Content-Type: application/json" \
-d '{"username":"jdoe","password":"secret","udId":"device-1"}'
# 2. Use the token to fetch the employee list
curl http://localhost:{port}/api/login/GetEmployeeList?userId=1 \
-H "Authorization: Bearer <token-from-step-1>"This project targets .NET Framework 4.6.1, which was the enterprise standard at the time of writing. The architecture patterns used here are fully applicable to modern .NET; the migration changes are surface-level framework API differences, not design changes:
| Concept | .NET Framework 4.6.1 | .NET 8 Equivalent |
|---|---|---|
| Hosting | Global.asax, WebApiConfig |
WebApplication.CreateBuilder() minimal API or controller app |
| Dependency Injection | Ninject (NinjectWebCommon.cs) |
Built-in IServiceCollection / AddScoped<> |
| JWT Authentication | Custom AuthorizationFilterAttribute |
Microsoft.AspNetCore.Authentication.JwtBearer middleware |
| Logging | Manual Trace.TraceError |
ILogger<T> via built-in logging pipeline |
| Configuration | Web.config / ConfigurationManager |
appsettings.json / IConfiguration |
| ORM | EF 6.x, Dapper, ADO.NET | EF Core, Dapper (unchanged), Microsoft.Data.SqlClient |
The n-tier structure, repository pattern, interface-based DI, and BOResponse<T> envelope pattern all carry over directly. Migrating this project to .NET 8 would be a natural next step.