Puedes ver el contenido de este vídeo junto con su curso en el modo vídeo (similar a Udemy) si pulsas aquí.

Runtime Async en .NET 11

22 Feb 2026 5 min (0) Comentarios

Hace un par de semanas el equipo de .NET lanzó la preview 1 de lo que va a ser .NET 11 y viene con un cambio que puede significar un gran salto de calidad para el ecosistema de .NET ya que planean cambiar la forma en la que async/await funciona por detrás. Enlace al blog de microsoft

 

 

 

 

1 - Qué es Runtime Async? 

 

Si trabajas en .NET lo mas normal es que conozcas que es async/await y cuando utilizarlo, cada vez que necesitas que tu código continue su ejecución en el exterior ya sea un servicio externo, consultar una api o poner un elemento en una cola vamos a estar utilizando async/await.

Y así hemos trabajado siempre (los que llevéis más tiempo, desde C# 5), al escribir async el compilador hace todo el trabajo por nosotros y lo que hace es generar un state machine que indica donde estamos en el método, las variables locales y que es lo que sucede cuando el await termina.

 

Así es como luce un código decompilado que contiene async/await: 

Codigo en C#:

private async Task<Result<string>> ValidateFlag(string flagName)
{
    bool flagExist = await applicationDbContext.Flags
        .Where(a => a.Name.Equals(flagName, StringComparison.InvariantCultureIgnoreCase))
        .AnyAsync();

    if (flagExist)
    {
        return Result.Failure<string>("Flag Name Already Exist");
    }
    return flagName;
}

 

Compilado: 

[AsyncStateMachine(typeof (AddFlagUseCase.<ValidateFlag>d__4))]
[DebuggerStepThrough]
[return: Nullable(new byte[] {1, 0, 1})]
private Task<Result<string>> ValidateFlag(string flagName)
{
  AddFlagUseCase.<ValidateFlag>d__4 stateMachine = new AddFlagUseCase.<ValidateFlag>d__4();
  stateMachine.<>t__builder = AsyncTaskMethodBuilder<Result<string>>.Create();
  stateMachine.<>4__this = this;
  stateMachine.flagName = flagName;
  stateMachine.<>1__state = -1;
  stateMachine.<>t__builder.Start<AddFlagUseCase.<ValidateFlag>d__4>(ref stateMachine);
  return stateMachine.<>t__builder.Task;
}

Además del State machine:

[CompilerGenerated]
private sealed class <ValidateFlag>d__4 : 
/*[Nullable(0)]*/
IAsyncStateMachine
{
  public int <>1__state;
  [Nullable(new byte[] {0, 0, 1})]
  public AsyncTaskMethodBuilder<Result<string>> <>t__builder;
  [Nullable(0)]
  public string flagName;
  [Nullable(0)]
  public AddFlagUseCase <>4__this;
  [Nullable(0)]
  private AddFlagUseCase.<>c__DisplayClass4_0 <>8__1;
  private bool <flagExist>5__2;
  private bool <>s__3;
  [Nullable(0)]
  private TaskAwaiter<bool> <>u__1;

  public <ValidateFlag>d__4()
  {
    base..ctor();
  }

  void IAsyncStateMachine.MoveNext()
  {
    int num1 = this.<>1__state;
    Result<string> result;
    try
    {
      TaskAwaiter<bool> awaiter;
      int num2;
      if (num1 != 0)
      {
        this.<>8__1 = new AddFlagUseCase.<>c__DisplayClass4_0();
        this.<>8__1.flagName = this.flagName;
        DbSet<FlagEntity> flags = this.<>4__this.<applicationDbContext>P.Flags;
        ParameterExpression parameterExpression = Expression.Parameter(typeof (FlagEntity), "a");
        Expression<Func<FlagEntity, bool>> predicate = Expression.Lambda<Func<FlagEntity, bool>>((Expression) Expression.Call((Expression) Expression.Property((Expression) parameterExpression, (MethodInfo) MethodBase.GetMethodFromHandle(__methodref (FlagEntity.get_Name))), (MethodInfo) MethodBase.GetMethodFromHandle(__methodref (string.Equals)), new Expression[2]
        {
          (Expression) Expression.Field((Expression) Expression.Constant((object) this.<>8__1, typeof (AddFlagUseCase.<>c__DisplayClass4_0)), FieldInfo.GetFieldFromHandle(__fieldref (AddFlagUseCase.<>c__DisplayClass4_0.flagName))),
          (Expression) Expression.Constant((object) StringComparison.InvariantCultureIgnoreCase, typeof (StringComparison))
        }), new ParameterExpression[1]
        {
          parameterExpression
        });
        awaiter = flags.Where<FlagEntity>(predicate).AnyAsync<FlagEntity>(new CancellationToken()).GetAwaiter();
        if (!awaiter.IsCompleted)
        {
          this.<>1__state = num2 = 0;
          this.<>u__1 = awaiter;
          AddFlagUseCase.<ValidateFlag>d__4 stateMachine = this;
          this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<bool>, AddFlagUseCase.<ValidateFlag>d__4>(ref awaiter, ref stateMachine);
          return;
        }
      }
      else
      {
        awaiter = this.<>u__1;
        this.<>u__1 = new TaskAwaiter<bool>();
        this.<>1__state = num2 = -1;
      }
      this.<>s__3 = awaiter.GetResult();
      this.<flagExist>5__2 = this.<>s__3;
      if (this.<flagExist>5__2)
        result = Result.Failure<string>("Flag Name Already Exist");
      else
        result = Result<string>.op_Implicit(this.<>8__1.flagName);
    }
    catch (Exception ex)
    {
      this.<>1__state = -2;
      this.<>8__1 = (AddFlagUseCase.<>c__DisplayClass4_0) null;
      this.<>t__builder.SetException(ex);
      return;
    }
    this.<>1__state = -2;
    this.<>8__1 = (AddFlagUseCase.<>c__DisplayClass4_0) null;
    this.<>t__builder.SetResult(result);
  }

  [DebuggerHidden]
  void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
  {
  }
}

Como puedes ver, no es un código elegante pero como el compilador lo hace por nosotros y es muy rápido, pues bueno, lo dejamos pasar. 

 

Con Runtime Async cambiamos esto, en vez del compilador generar un state machine, es el propio runtime quien está encargado de la ejecución asíncrona, tanto por el JIT como por el motor de ejecución.

y para habilitarlo únicamente debemos utilizar .NET 11 y habilitar la funcionalidad en la previeiw 

<PropertyGroup>
    <TargetFramework>net11.0</TargetFramework>
...
    <EnablePreviewFeatures>true</EnablePreviewFeatures>
    <Features>$(Features);runtime-async=on</Features>
</PropertyGroup>

NOTA: la parte de preview no será necesaria una vez esté lanzado de forma oficial.

 

2 - Qué mejoras puede traer? 

 

La gran pregunta aquí es por qué nos puede interesar esto a nosotros los desarrolladores normales. La respuesta en pocas palabras es muy sencilla: Mejoras de código prácticamente gratis.

 

Pero si nos adentramos un poco más allá tenemos tres puntos principales:

 

A - Rendimiento

Posiblemente el principal en aplicaciones que tienen miles de llamadas por minuto. Al generar el state machine, ubicamos objetos principalmente en el heap. Cada método async que no se completa de forma síncrona crea una objeto Task, un state machine y muy posiblemente otros objetos, lo cual, si tenemos miles y miles de llamadas por minuto, pues se nota. 

 

Recientemente (creo que 5 años o así) aprendimos  usar ValueTask<T> en vez de Task<T> para evitar allocations cuando el método se completa de forma síncrona, usar ConfigureAwait(false) en librerías, cachear Tasks para resultados comunes... Todas estas optimizaciones siguen siendo válidas, pero no todo el mundo tiene el tiempo ni la necesidad de exprimir cada allocation de su código async.

 

La idea de Runtime Async es que el runtime trabaje de una forma más eficiente, reduciendo las asignaciones de memoria o incluso eliminándolas completamente. Lo interesante aquí es que la diferencia entre "código async optimizado manualmente" y "simplemente escribir async/await normal" se reduce muchísimo ya que es el runtime quien lo hace por nosotros.

 

 

B - Facilidad para debuguear

Entiendo que si estás leyendo esto es porque te interesa y te has peleado mucho tiempo con el código, y sabrás también que C# es famoso por tener un stacktrace enorme, el cual es mucho más grande cuando trabajamos con métodos async es incluso peor y se hace difícil navegar por él. 

Con el runtime siendo quien administra async de forma nativa la experiencia de debuggerar debería también mejorar.

 

 

C - Menor código IL

Muchos miramos de vez en cuando el código IL generado para ver cómo funcionan las cosas por detrás. Esta parte viene relacionada con el punto 1, donde teníamos un método donde veíamos que el compilador nos había generado el state machine. 

 

Este código es el mismo método en .NET 11 con Runtime async habilitado: 

[MethodImpl(MethodImplOptions.Async)]
[return: Nullable(new byte[] {1, 0})]
private Task<Result<bool>> ADdFlagToDatabase(string flagName, bool isActive)
{
  FlagEntity entity = new FlagEntity();
  entity.Name = flagName;
  entity.UserId = this.<flagUserDetails>P.UserId;
  entity.Value = isActive;
  AsyncHelpers.Await<EntityEntry<FlagEntity>>(this.<applicationDbContext>P.Flags.AddAsync(entity, new CancellationToken()));
  AsyncHelpers.Await<int>(this.<applicationDbContext>P.SaveChangesAsync(new CancellationToken()));
  return (Task<Result<bool>>) Result<bool>.op_Implicit(true);
}

Menos código siempre es mejor, el JIT va a ser más rápido, va a necesitar menos memoria y para quienes utilicen serverless, va a ser mucho más rápido. 

 

 

3 - Cuando lo vamos a poder usar? 

 

Esta nueva característica son buenas noticias para prácticamente todos los desarrolladores ya que no todo el mundo tiene tiempo de optimizar cada proceso al máximo y de esta forma lo hace el runtime, o bueno el equipo de .NET por nosotros. Lo cual es maravilloso y algo a agradecer al equipo de .NET, que nos da todas estas ventajas sin afectar al código existente. 

 

Dicho esto, esto es la Preview 1, lo que significa que las cosas pueden cambiar y que puede haber bugs, edge cases que no fucnionan etcetera, pero es definitivamente una funcionalidad esperada, ya que hace tiempo que la comunidad se viene quejando de como funcionaba async/await por detrás. 

 

Así que cuando salga .NET 11 en noviembre o quizá para .NET 12 con la LTS espero tener Runtime Async completamente pulido!

 

 

© copyright 2026 NetMentor | Todos los derechos reservados | RSS Feed

Buy me a coffee Invitame a un café

🎨 Nueva Interfaz Disponible

¡Estamos probando una nueva interfaz con estilo Neo Brutalismo!

Esta es una versión en desarrollo que incluye:

  • ✨ Diseño moderno y audaz
  • 🎯 Mejor experiencia visual
  • 📱 Interfaz más limpia

¿Cómo activarla?

Añade ?useNewUI=true al final de cualquier URL

Ejemplo: https://netmentor.es?useNewUI=true

¿Cómo volver a la versión anterior?

Añade ?useNewUI=false al final de cualquier URL

⚠️ Esta es una versión en progreso. Algunos elementos pueden no funcionar perfectamente.