DevOps & Development 6 min de lectura 16 de octubre de 2025

Limitar resultados de un LinqDataSource en ASP.NET

Aprende a implementar consultas dinámicas tipo TOP N y filtros avanzados en LinqDataSource mediante propiedades públicas en Web Forms.

Solución en "Dos Clics" (TL;DR)

Cómo implementar una consulta tipo 'TOP N' en LinqDataSource mediante el evento Selecting para limitar registros en Web Forms.

Durante el desarrollo de aplicaciones con ASP.NET Web Forms, es común encontrarse con la necesidad de limitar la cantidad de registros devueltos por un control LinqDataSource. A diferencia de SQL tradicional, este control no cuenta con una propiedad directa como Top. En esta bitácora, detallo cómo logré implementar un límite dinámico tipo "TOP N" utilizando el evento Selecting, cómo transformé un control de usuario rígido en un componente altamente configurable y cómo superé un clásico dolor de cabeza del diseñador de Web Forms.

1. El desafío inicial: Simular un TOP N en LinqDataSource

El control LinqDataSource es excelente para enlazar datos rápidamente, pero carece de una propiedad nativa para limitar el número de filas devueltas. Para simular un TOP 10, la clave está en interceptar la consulta antes de que se ejecute en la base de datos. Esto se logra mediante el evento Selecting.

Al capturar el evento, podemos manipular la consulta LINQ directamente y aplicar el método .Take(N) antes de asignar el resultado a e.Result:

Archivo de código (VB.NET)
Protected Sub LinqDataSource1_Selecting(sender As Object, e As LinqDataSourceSelectEventArgs)
    Dim db As New TuDataContext()

    Dim query = From p In db.Productos
                Order By p.FechaCreacion Descending
                Select p

    e.Result = query.Take(10).ToList()
End Sub
Consejo Práctico: Siempre es recomendable aplicar un Order By antes del Take para garantizar que los resultados devueltos sigan un criterio lógico (por ejemplo, los registros más recientes o alfabéticos). Además, asegúrate de aplicar el límite antes de llamar a .ToList() para que la paginación se resuelva de manera eficiente a nivel de base de datos (SQL) y no en memoria.

2. Evolucionando a un Control de Usuario (User Control) Configurable

El escenario real se complicó cuando necesité aplicar este límite dentro de un control de usuario personalizado (wucEstudiantes.ascx). Si definía un límite estático de 5 o 10 registros dentro del control, afectaría a todas las pantallas que lo consumían. La solución elegante fue exponer una propiedad pública desde el código del control de usuario (code-behind) para permitir que la página contenedora definiera el límite bajo demanda.

Primero, definí la propiedad en el archivo wucEstudiantes.ascx.vb:

wucEstudiantes.ascx.vb
Private _LimiteResultados As Integer = 10 ' Valor por defecto

Public Property LimiteResultados As Integer
    Get
        Return _LimiteResultados
    End Get
    Set(value As Integer)
        _LimiteResultados = value
    End Set
End Property

Luego, modifiqué el método Selecting del control para consumir esta propiedad:

wucEstudiantes.ascx.vb (Selecting)
Protected Sub ldsList_Selecting(sender As Object, e As System.Web.UI.WebControls.LinqDataSourceSelectEventArgs) Handles ldsList.Selecting
    Dim db As New dtcVistasDataContext

    Dim query = From tbl In db.vwEstudiantes
                Where tbl.Nombre.Contains(Me.wucBusqueda1.TextoBuscar) _
                   Or tbl.status.Contains(Me.wucBusqueda1.TextoBuscar) _
                   Or tbl.motive.Contains(Me.wucBusqueda1.TextoBuscar) _
                   Or tbl.Identificacion.Contains(Me.wucBusqueda1.TextoBuscar) _
                   Or tbl.Carrera.Contains(Me.wucBusqueda1.TextoBuscar) _
                   Or tbl.NoCarne.Contains(Me.wucBusqueda1.TextoBuscar)
                Select tbl

    e.Result = query.Take(Me.LimiteResultados).ToList()
End Sub

De este modo, desde la página .aspx.vb que aloja el control, basta con establecer el valor deseado durante el Page_Load:

Formulario.aspx.vb
Protected Sub Page_Load(sender As Object, e As EventArgs) Handles Me.Load
    If Not IsPostBack Then
        Me.wucEstudiantes1.LimiteResultados = 5
    End If
End Sub

3. Filtros condicionales avanzados (Estado y Carrera)

Con la infraestructura de propiedades públicas funcionando, surgió un nuevo requerimiento: el control de usuario también debía filtrar opcionalmente por campos específicos como status = 1 o Carrera = "Medicina". Para lograr esto sin romper la flexibilidad del control, implementé una consulta dinámica utilizando AsQueryable().

Definí las nuevas propiedades públicas en el control de usuario:

wucEstudiantes.ascx.vb (Filtros)
Private _FiltrarPorStatus As Boolean = False
Private _StatusValor As Integer = 1

Public Property FiltrarPorStatus As Boolean
    Get
        Return _FiltrarPorStatus
    End Get
    Set(value As Boolean)
        _FiltrarPorStatus = value
    End Set
End Property

Public Property StatusValor As Integer
    Get
        Return _StatusValor
    End Get
    Set(value As Integer)
        _StatusValor = value
    End Set
End Property

Private _FiltrarPorCarrera As Boolean = False
Private _CarreraValor As String = ""

Public Property FiltrarPorCarrera As Boolean
    Get
        Return _FiltrarPorCarrera
    End Get
    Set(value As Boolean)
        _FiltrarPorCarrera = value
    End Set
End Property

Public Property CarreraValor As String
    Get
        Return _CarreraValor
    End Get
    Set(value As String)
        _CarreraValor = value
    End Set
End Property

Y reescribí el evento Selecting para concatenar los filtros de manera condicional:

wucEstudiantes.ascx.vb (Selecting Dinámico)
Protected Sub ldsList_Selecting(sender As Object, e As System.Web.UI.WebControls.LinqDataSourceSelectEventArgs) Handles ldsList.Selecting
    Dim db As New dtcVistasDataContext
    Dim query = db.vwEstudiantes.AsQueryable()

    If Not String.IsNullOrEmpty(Me.wucBusqueda1.TextoBuscar) Then
        query = query.Where(Function(tbl) _
            tbl.Nombre.Contains(Me.wucBusqueda1.TextoBuscar) _
            Or tbl.status.Contains(Me.wucBusqueda1.TextoBuscar) _
            Or tbl.motive.Contains(Me.wucBusqueda1.TextoBuscar) _
            Or tbl.Identificacion.Contains(Me.wucBusqueda1.TextoBuscar) _
            Or tbl.Carrera.Contains(Me.wucBusqueda1.TextoBuscar) _
            Or tbl.NoCarne.Contains(Me.wucBusqueda1.TextoBuscar))
    End If

    If Me.FiltrarPorStatus Then
        query = query.Where(Function(tbl) tbl.status = Me.StatusValor.ToString())
    End If

    If Me.FiltrarPorCarrera AndAlso Not String.IsNullOrEmpty(Me.CarreraValor) Then
        query = query.Where(Function(tbl) tbl.Carrera = Me.CarreraValor)
    End If

    e.Result = query.OrderBy(Function(tbl) tbl.Nombre).Take(Me.LimiteResultados).ToList()
End Sub

Ahora, desde la página contenedora, el control se puede configurar con total precisión:

Formulario.aspx.vb (Configuración)
Protected Sub Page_Load(sender As Object, e As EventArgs) Handles Me.Load
    If Not IsPostBack Then
        With Me.wucEstudiantes1
            .LimiteResultados = 10
            .FiltrarPorStatus = True
            .StatusValor = 1
            .FiltrarPorCarrera = True
            .CarreraValor = "Medicina"
        End With
    End If
End Sub

4. Haciendo el límite de resultados realmente opcional

¿Qué pasa si en alguna pantalla no queremos limitar los resultados? En VB.NET, las propiedades numéricas estándar no pueden omitirse fácilmente sin asumir un valor por defecto (como 0 o 10). Para solucionar esto y permitir un comportamiento "sin límite", cambié el tipo de la propiedad a un tipo anulable (Nullable(Of Integer) o Integer?):

wucEstudiantes.ascx.vb (Anulable)
Private _LimiteResultados As Integer? = Nothing

Public Property LimiteResultados As Integer?
    Get
        Return _LimiteResultados
    End Get
    Set(value As Integer?)
        _LimiteResultados = value
    End Set
End Property

Con este cambio, ajusté la lógica del evento para aplicar el Take únicamente si la propiedad tiene un valor asignado:

wucEstudiantes.ascx.vb (Lógica de Límite)
If Me.LimiteResultados.HasValue AndAlso Me.LimiteResultados.Value > 0 Then
    e.Result = query.OrderBy(Function(tbl) tbl.Nombre).Take(Me.LimiteResultados.Value).ToList()
Else
    e.Result = query.OrderBy(Function(tbl) tbl.Nombre).ToList()
End If

5. El error del compilador: "is not a member of 'controles_wucestudiantes.ascx'"

Al intentar consumir las nuevas propiedades desde el formulario, me topé con un error frustrante durante la compilación:

Error detectado: 'LimiteResultados' is not a member of 'controles_wucestudiantes.ascx'. El compilador reconocía la existencia del control, pero se negaba a ver las nuevas propiedades públicas.

Este comportamiento es un clásico "desajuste" del diseñador de Web Forms. Ocurre cuando el archivo de diseño (.designer.vb) o la clase autogenerada se desincronizan del archivo de código subyacente (.ascx.vb). Aunque la propiedad esté declarada como Public, el entorno no refresca las referencias.

Tras intentar limpiar la solución y reconstruir el proyecto sin éxito, apliqué la solución definitiva y más rápida para este bug del IDE: eliminar el control de usuario del formulario .aspx y volverlo a arrastrar/escribir. Al hacer esto, Visual Studio se vio obligado a regenerar la clase parcial del diseñador, reconociendo de inmediato todas las nuevas propiedades expuestas.

La historia detrás de la nota

A veces pasamos horas depurando código impecable cuando el verdadero culpable es el propio entorno de desarrollo. En Web Forms, un simple "quitar y poner" sigue siendo un recurso de soporte técnico de primer nivel.