Dropdown Lists in ASP.NET MVC: A Comprehensive Guide
Dropdown Lists in ASP.NET MVC and ASP.NET Core: A Practical Guide
Dropdown lists are one of the most common form controls in ASP.NET applications. They appear in registration forms, product filters, administrative screens, search interfaces, order forms, dashboards, and almost any application where users need to choose one value from a predefined set of options.
Although creating a basic HTML <select> element is straightforward, real applications usually need more. The options may come from a database, the selected value may need to be restored while editing a record, one dropdown may depend on another, and the selected value must be correctly bound and validated when the form is submitted.
ASP.NET MVC and ASP.NET Core provide built-in tools for handling these scenarios.
This guide explains how dropdown lists work in both traditional ASP.NET MVC and modern ASP.NET Core MVC, with practical C# examples covering static lists, database-driven dropdowns, model binding, validation, edit forms, and dynamic cascading dropdowns.
What Is a Dropdown List in ASP.NET MVC?
A dropdown list is rendered in HTML using a <select> element containing one or more <option> elements.
A simple HTML example looks like this:
<select name="CountryId">
<option value="">Select a country</option>
<option value="1">United States</option>
<option value="2">Canada</option>
<option value="3">United Kingdom</option>
</select>ASP.NET MVC builds on this standard HTML behavior by connecting the selected value to your C# model.
Depending on the version of ASP.NET you are using, you will commonly work with:
Html.DropDownListForSelectListSelectListItemASP.NET Core Select Tag Helper
asp-forasp-items
The important principle is the same in each case: the application provides a collection of options, displays those options to the user, and binds the selected value back to a model property.
ASP.NET MVC vs ASP.NET Core Dropdown Lists
Traditional ASP.NET MVC applications commonly use HTML Helpers:
@Html.DropDownListFor(
model => model.CountryId,
Model.Countries,
"Select a country"
)Modern ASP.NET Core MVC applications can still use HTML Helpers, but the Select Tag Helper provides a cleaner Razor syntax:
<select asp-for="CountryId"
asp-items="Model.Countries"
class="form-select">
<option value="">Select a country</option>
</select>Here:
asp-for identifies the model property that stores the selected value.
asp-items provides the available options.
This makes dropdowns work naturally with ASP.NET Core model binding and validation.
Create a Strongly Typed Dropdown View Model
For most applications, a strongly typed view model is preferable to passing dropdown data through ViewBag.
Consider a form where a user selects a country.
using Microsoft.AspNetCore.Mvc.Rendering;
using System.ComponentModel.DataAnnotations;
public class CustomerViewModel
{
public int Id { get; set; }
[Required(ErrorMessage = "Please select a country.")]
public int? CountryId { get; set; }
public IEnumerable<SelectListItem> Countries { get; set; }
= Enumerable.Empty<SelectListItem>();
}There are two separate responsibilities here.
CountryId contains the value selected by the user.
Countries contains the options displayed in the dropdown.
Keeping these responsibilities separate makes the form easier to understand, validate, test, and maintain.
Creating a Static Dropdown List
If the available options rarely change, you can build them directly in C#.
public IActionResult Create()
{
var model = new CustomerViewModel
{
Countries = new List<SelectListItem>
{
new()
{
Value = "1",
Text = "United States"
},
new()
{
Value = "2",
Text = "Canada"
},
new()
{
Value = "3",
Text = "United Kingdom"
}
}
};
return View(model);
}Then render the dropdown in the Razor view:
<div class="mb-3">
<label asp-for="CountryId" class="form-label"></label>
<select asp-for="CountryId"
asp-items="Model.Countries"
class="form-select">
<option value="">Select a country</option>
</select>
<span asp-validation-for="CountryId"
class="text-danger"></span>
</div>When the form is submitted, ASP.NET Core automatically attempts to bind the selected value to CountryId.
Using Html.DropDownListFor
If you prefer HTML Helpers, or you maintain an existing ASP.NET MVC application, DropDownListFor remains useful.
@Html.DropDownListFor(
model => model.CountryId,
Model.Countries,
"Select a country",
new { @class = "form-select" }
)This produces a standard HTML <select> element.
The first expression identifies the property that receives the selected value, while Model.Countries supplies the available options.
Strongly typed helpers are generally preferable to manually managing field names because they keep the view connected to the model.
Populate an ASP.NET Core Dropdown from a Database
Real business applications usually load dropdown options from a database.
Suppose the application has this entity:
public class Country
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
}You can retrieve the required values using Entity Framework Core:
public async Task<IActionResult> Create()
{
var countries = await _context.Countries
.OrderBy(x => x.Name)
.Select(x => new SelectListItem
{
Value = x.Id.ToString(),
Text = x.Name
})
.ToListAsync();
var model = new CustomerViewModel
{
Countries = countries
};
return View(model);
}The Razor view remains simple:
<select asp-for="CountryId"
asp-items="Model.Countries"
class="form-select">
<option value="">Select a country</option>
</select>This pattern works well for categories, suppliers, departments, product types, warehouses, customers, employees, payment methods, and many other business entities.
Why Project Only the Fields You Need?
A dropdown usually needs only two values:
the option value
the text shown to the user
If your Country entity contains many additional properties, there is usually no reason to retrieve all of them just to populate a dropdown.
Instead of loading complete entities and transforming them afterward, project the required values directly:
var countries = await _context.Countries
.OrderBy(x => x.Name)
.Select(x => new SelectListItem
{
Value = x.Id.ToString(),
Text = x.Name
})
.ToListAsync();This keeps the database query focused on the data the interface actually needs.
Handling the Selected Value on Form Submission
Suppose the user submits this form:
<form asp-action="Create" method="post">
<div class="mb-3">
<label asp-for="CountryId"></label>
<select asp-for="CountryId"
asp-items="Model.Countries"
class="form-select">
<option value="">Select a country</option>
</select>
<span asp-validation-for="CountryId"
class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">
Save
</button>
</form>The controller receives the selected value through model binding:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(CustomerViewModel model)
{
if (!ModelState.IsValid)
{
model.Countries = await GetCountriesAsync();
return View(model);
}
var customer = new Customer
{
CountryId = model.CountryId!.Value
};
_context.Customers.Add(customer);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}A reusable method can populate the dropdown:
private async Task<List<SelectListItem>> GetCountriesAsync()
{
return await _context.Countries
.OrderBy(x => x.Name)
.Select(x => new SelectListItem
{
Value = x.Id.ToString(),
Text = x.Name
})
.ToListAsync();
}This is an important pattern because dropdown collections themselves are not normally posted back as part of your model.
If validation fails and you return the same view, populate the list again before rendering it.
Dropdown Validation in ASP.NET Core
Dropdown validation works naturally with model validation.
For example:
[Required(ErrorMessage = "Please select a country.")]
public int? CountryId { get; set; }The view can display the validation message:
<select asp-for="CountryId"
asp-items="Model.Countries"
class="form-select">
<option value="">Select a country</option>
</select>
<span asp-validation-for="CountryId"
class="text-danger"></span>Using a nullable int? is useful when the initial option has an empty value:
<option value="">Select a country</option>Until the user selects a valid country, CountryId can remain null and [Required] can reject the form.
Keep the Selected Value When Editing Data
Dropdowns are commonly used on Edit pages.
Suppose an existing customer has CountryId = 2.
Load the entity and build the view model:
public async Task<IActionResult> Edit(int id)
{
var customer = await _context.Customers
.FindAsync(id);
if (customer == null)
{
return NotFound();
}
var model = new CustomerViewModel
{
Id = customer.Id,
CountryId = customer.CountryId,
Countries = await GetCountriesAsync()
};
return View(model);
}Then:
<select asp-for="CountryId"
asp-items="Model.Countries"
class="form-select">
<option value="">Select a country</option>
</select>Because CountryId already contains the customer's value, ASP.NET Core can render the matching option as selected.
This is one of the advantages of keeping the selected value in the view model instead of manually constructing HTML.
Dynamic Dropdown Lists
Not every dropdown should contain a fixed set of values.
A dynamic dropdown may depend on:
data stored in a database
the current user
another dropdown
an API response
the selected organization or tenant
permissions
previous form selections
A common example is:
Country → State/Province
The available states should change when the user changes the country.
This is commonly called a dependent or cascading dropdown.
Cascading Dropdown Lists in ASP.NET Core
Start with the country dropdown:
<select asp-for="CountryId"
asp-items="Model.Countries"
class="form-select"
id="countryDropdown">
<option value="">Select a country</option>
</select>Then create the state dropdown:
<select asp-for="StateId"
class="form-select"
id="stateDropdown">
<option value="">Select a state</option>
</select>Create an endpoint that returns states for a country:
[HttpGet]
public async Task<IActionResult> GetStates(int countryId)
{
var states = await _context.States
.Where(x => x.CountryId == countryId)
.OrderBy(x => x.Name)
.Select(x => new
{
value = x.Id,
text = x.Name
})
.ToListAsync();
return Json(states);
}Then update the second dropdown when the country changes:
const countryDropdown =
document.getElementById("countryDropdown");
const stateDropdown =
document.getElementById("stateDropdown");
countryDropdown.addEventListener("change", async function () {
const countryId = this.value;
stateDropdown.innerHTML =
'<option value="">Select a state</option>';
if (!countryId) {
return;
}
const response =
await fetch(`/Customers/GetStates?countryId=${countryId}`);
if (!response.ok) {
return;
}
const states = await response.json();
states.forEach(state => {
const option = document.createElement("option");
option.value = state.value;
option.textContent = state.text;
stateDropdown.appendChild(option);
});
});Now the second dropdown is populated without reloading the entire page.
Why asp-items Does Not Work in Dynamically Created Browser Elements
An important distinction is that Tag Helpers such as asp-items run on the server while Razor is rendering the page.
For example:
<select asp-for="CountryId"
asp-items="Model.Countries">
</select>is processed before the HTML reaches the browser.
If JavaScript later creates a new <select> element in the browser, adding an asp-items attribute to that element will not cause ASP.NET Core to process it again.
For client-side dynamic dropdowns, retrieve the required data from an endpoint and create the <option> elements using JavaScript.
Using Enum Values in a Dropdown
Enums can also be presented as dropdown lists.
Suppose you have:
public enum OrderStatus
{
Pending,
Processing,
Completed,
Cancelled
}And your model contains:
public OrderStatus Status { get; set; }ASP.NET Core can generate the options:
<select asp-for="Status"
asp-items="Html.GetEnumSelectList<OrderStatus>()"
class="form-select">
</select>This is convenient for relatively small, fixed sets of application states.
Using Display Names with Enum Dropdowns
Sometimes enum member names are appropriate for code but not ideal for users.
You can use Display:
public enum PaymentStatus
{
[Display(Name = "Awaiting Payment")]
AwaitingPayment,
[Display(Name = "Payment Received")]
Paid,
[Display(Name = "Payment Failed")]
Failed
}Then:
<select asp-for="PaymentStatus"
asp-items="Html.GetEnumSelectList<PaymentStatus>()"
class="form-select">
</select>This keeps developer-friendly enum names while presenting clearer labels to users.
Using ViewBag for Dropdown Lists
You will also encounter code such as:
ViewBag.Countries = new SelectList(
countries,
"Id",
"Name"
);with:
@Html.DropDownListFor(
model => model.CountryId,
(SelectList)ViewBag.Countries,
"Select a country"
)This works, particularly in older ASP.NET MVC projects.
However, for larger applications, a strongly typed view model is usually easier to maintain because the compiler can help identify mistakes and the view's dependencies are explicit.
For new ASP.NET Core applications, prefer strongly typed models unless there is a good reason not to.
Common Dropdown List Problems
The dropdown is empty
Check that the option collection is populated before the view is returned.
model.Countries = await GetCountriesAsync();
return View(model);The dropdown becomes empty after validation fails
This usually happens because the POST action returns the view without rebuilding the options.
Incorrect:
if (!ModelState.IsValid)
{
return View(model);
}Better:
if (!ModelState.IsValid)
{
model.Countries = await GetCountriesAsync();
return View(model);
}The selected value is not restored on Edit
Make sure the model property contains the existing value before rendering the page.
model.CountryId = customer.CountryId;The selected value is always zero
If zero does not represent a valid selection, consider using a nullable property:
public int? CountryId { get; set; }with:
<option value="">Select a country</option>Dropdown values and model property types do not match
If your option values contain integer IDs, the bound model property should normally be an integer or nullable integer.
For example:
public int? CountryId { get; set; }Avoid unnecessary string conversion throughout your business logic.
Searchable Dropdowns for Large Data Sets
A normal dropdown works well when the number of options is reasonably small.
It becomes less useful when you have hundreds or thousands of:
customers
products
suppliers
cities
inventory items
employees
Loading thousands of <option> elements can create a poor user experience and unnecessarily increase page size.
For large datasets, consider a searchable autocomplete or remote-loading component.
A better architecture is often:
User types search text
↓
Browser sends query
↓
ASP.NET Core endpoint
↓
Database returns limited matches
↓
User selects an itemThis avoids loading an entire large table simply to populate one control.
Performance Tips for Database Dropdowns
Dropdown queries are usually simple, but they can become expensive in large applications.
Retrieve only required fields
Use projection:
.Select(x => new SelectListItem
{
Value = x.Id.ToString(),
Text = x.Name
})Sort options deliberately
.OrderBy(x => x.Name)Users should not have to search through an unpredictable list.
Filter inactive records
If only active countries, suppliers, products, or employees should be selectable, filter them at the database level:
.Where(x => x.IsActive)Use asynchronous database calls
await query.ToListAsync();This is especially appropriate for database-backed ASP.NET Core applications.
Avoid huge dropdowns
If the option count becomes large, move to search/autocomplete rather than rendering every row.
Accessibility Best Practices
A dropdown should have a clear associated label:
<label asp-for="CountryId"
class="form-label">
</label>
<select asp-for="CountryId"
asp-items="Model.Countries"
class="form-select">
<option value="">Select a country</option>
</select>Avoid relying only on placeholder-style text to explain what the field means.
Also make sure:
keyboard navigation works
labels clearly describe the field
validation messages are understandable
option text is meaningful
disabled options are used only when appropriate
Native HTML select elements already provide useful browser and accessibility behavior, so avoid replacing them with custom components unless the application genuinely needs additional functionality.
ASP.NET MVC Dropdown List Best Practices
For maintainable applications:
Use strongly typed view models for form data and dropdown options.
Keep the selected value separate from the collection of available options.
Use
asp-forandasp-itemsin modern ASP.NET Core MVC applications.Use
DropDownListForwhen appropriate in existing MVC applications.Populate dropdown collections again when returning a form after validation failure.
Query only the database fields required for the options.
Use nullable properties when an empty initial selection is valid before submission.
Use cascading dropdowns when one selection determines another.
Use autocomplete or server-side search for very large datasets.
Validate selected values on the server rather than trusting browser input.
Frequently Asked Questions
How do I create a dropdown list in ASP.NET Core MVC?
Create a property for the selected value and an IEnumerable<SelectListItem> for the options. Then use the Select Tag Helper:
<select asp-for="CountryId"
asp-items="Model.Countries">
</select>What is the difference between asp-for and asp-items?
asp-for identifies the model property associated with the <select> element.
asp-items provides the options that should appear inside it.
Can I use DropDownListFor in ASP.NET Core?
Yes. ASP.NET Core supports HTML Helper alternatives such as Html.DropDownListFor, although the Select Tag Helper often provides cleaner Razor markup for modern applications.
How do I populate a dropdown from SQL Server or another database?
Query the required records using EF Core or your chosen data-access approach and transform them into SelectListItem objects containing a Value and Text.
The database provider does not fundamentally change the dropdown pattern, so the same approach can be used with SQL Server, PostgreSQL, MySQL, and other supported databases.
How do I keep the selected dropdown value after validation fails?
ASP.NET Core's model binding keeps the submitted selected value in ModelState, but you still need to rebuild the collection of available options before returning the view.
How do I create a dynamic dropdown in ASP.NET Core?
For database-driven values, populate the collection before rendering the view. For dependent dropdowns that change after the page has loaded, use JavaScript to call an ASP.NET Core endpoint and populate the options dynamically.
How do I create a cascading dropdown?
Listen for the first dropdown's change event, send the selected ID to an endpoint, retrieve the related records as JSON, and populate the second dropdown.
Should I use ViewBag for dropdown lists?
It works, but strongly typed view models are generally easier to maintain and less error-prone in larger applications.
How should I handle thousands of dropdown options?
Do not render thousands of options unless there is a strong reason. Use a searchable component or autocomplete endpoint that returns a limited number of matching records.
Conclusion
Dropdown lists may look like a small part of an ASP.NET application, but they connect several important concepts: Razor rendering, model binding, validation, database access, client-side interaction, and user experience.
For modern ASP.NET Core MVC applications, a strongly typed view model combined with asp-for, asp-items, and SelectListItem provides a clean foundation for most dropdown scenarios.
For existing ASP.NET MVC applications, Html.DropDownListFor remains a practical approach and follows the same core idea: bind a collection of available options to a model property that stores the selected value.
Once you understand that pattern, you can move naturally from simple static lists to database-backed dropdowns, Edit forms, validation, enums, cascading selections, and searchable controls for large datasets.
Comments 0