Evento
Campionato do Mundo de Power BI DataViz
Feb 14, 4 PM - Mar 31, 4 PM
Con 4 posibilidades de entrar, poderías gañar un paquete de conferencias e facelo ao Live Grand Finale en Las Vegas
Máis informaciónEste explorador xa non é compatible.
Actualice a Microsoft Edge para dispoñer das funcionalidades máis recentes, as actualizacións de seguranza e a asistencia técnica.
Nota
Esta no es la versión más reciente de este artículo. Para la versión actual, consulte la versión de .NET 9 de este artículo.
Aviso
Esta versión de ASP.NET Core ya no se admite. Para obtener más información, consulte la directiva de compatibilidad de .NET y .NET Core. Para la versión actual, consulte la versión de .NET 9 de este artículo.
Importante
Esta información hace referencia a un producto en versión preliminar, el cual puede sufrir importantes modificaciones antes de que se publique la versión comercial. Microsoft no proporciona ninguna garantía, expresa o implícita, con respecto a la información proporcionada aquí.
Para la versión actual, consulte la versión de .NET 9 de este artículo.
Por Mike Rousos
En este artículo se proporcionan instrucciones para maximizar el rendimiento y la confiabilidad de las aplicaciones de ASP.NET Core.
El almacenamiento en caché se describe en varias partes de este artículo. Para más información, consulte Información general sobre el almacenamiento en caché en ASP.NET Core.
En este artículo, una ruta de acceso al código activa se define como una ruta de acceso al código a la que se llama con frecuencia y donde se produce gran parte del tiempo de ejecución. Las rutas de acceso al código activas suelen limitar la escalabilidad horizontal y el rendimiento de las aplicaciones y se describen en varias partes de este artículo.
Las aplicaciones de ASP.NET Core deben diseñarse para procesar muchas solicitudes simultáneamente. Las API asincrónicas permiten que un pequeño grupo de subprocesos controle miles de solicitudes simultáneas sin esperar a las llamadas de bloqueo. En lugar de esperar a que se complete una tarea sincrónica de larga duración, el subproceso puede trabajar en otra solicitud.
Un problema de rendimiento común en las aplicaciones de ASP.NET Core es que las llamadas de bloqueo podrían ser asincrónicas. Muchas llamadas de bloqueo sincrónicas conducen a la escasez del grupo de subprocesos y a tiempos de respuesta degradados.
No bloquee la ejecución asincrónica llamando a Task.Wait o Task<TResult>.Result.
No adquiera bloqueos en rutas de acceso al código comunes. Las aplicaciones de ASP.NET Core funcionan mejor cuando se diseñan para ejecutar código en paralelo.
No llame a Task.Run y espere inmediatamente. ASP.NET Core ya ejecuta código de aplicación en los subprocesos normales del grupo de subprocesos, por lo que la llamada a Task.Run
solo da lugar a la programación adicional innecesaria del grupo de subprocesos. Aunque el código programado bloquee un subproceso, Task.Run
no lo impide.
Un generador de perfiles, como PerfView, se puede usar para buscar subprocesos que se agregan con frecuencia al grupo de subprocesos. El evento Microsoft-Windows-DotNETRuntime/ThreadPoolWorkerThread/Start
indica que se agregó un subproceso al grupo de subprocesos.
Una página web no debe cargar grandes cantidades de datos a la vez. Al devolver una colección de objetos, tenga en cuenta si podría provocar problemas de rendimiento. Determine si el diseño podría producir los siguientes resultados deficientes:
Agregue paginación para mitigar los escenarios anteriores. Con el uso de parámetros de índice de página y tamaño de página, los desarrolladores deben favorecer el diseño de devolución de un resultado parcial. Cuando se requiere un resultado exhaustivo, la paginación se debe usar para rellenar de forma asincrónica lotes de resultados a fin de evitar el bloqueo de los recursos del servidor.
Para obtener más información sobre la paginación y la limitación del número de registros devueltos, consulte:
La devolución de IEnumerable<T>
en una acción da como resultado la iteración de la colección sincrónica por parte del serializador. El resultado es el bloqueo de llamadas y una posibilidad de colapso del grupo de subprocesos. Para evitar la enumeración sincrónica, use ToListAsync
antes de devolver el enumerable.
A partir de ASP.NET Core 3.0, IAsyncEnumerable<T>
se puede usar como alternativa a IEnumerable<T>
, que enumera de forma asincrónica. Para obtener más información, consulte Tipos de valor devuelto de acción del controlador.
El recolector de elementos no utilizados de .NET Core administra la asignación y liberación de memoria automáticamente en las aplicaciones de ASP.NET Core. Con la recolección automática de elementos no utilizados, normalmente los desarrolladores no necesitan preocuparse por cómo o cuándo se libera memoria. Sin embargo, la limpieza de objetos sin referencia tarda tiempo en la CPU, por lo que los desarrolladores deben minimizar la asignación de objetos en rutas de acceso al código activas. La recolección de elementos no utilizados es especialmente costosa en objetos grandes (>= 85.000 bytes). Los objetos grandes se almacenan en el montón de objetos grandes y requieren una recolección de elementos no utilizados completa (generación 2) para la limpieza. A diferencia de las colecciones de generación 0 y generación 1, una colección de generación 2 requiere una suspensión temporal de la ejecución de la aplicación. La asignación y desasignación frecuentes de objetos grandes puede provocar un rendimiento incoherente.
Recomendaciones:
Los problemas de memoria, como los anteriores, se pueden diagnosticar revisando las estadísticas de recolección de elementos no utilizados (GC) en PerfView y examinando lo siguiente:
Para obtener más información, consulte Recolección de elementos no utilizados y rendimiento.
Las interacciones con un almacén de datos y otros servicios remotos suelen ser las partes más lentas de una aplicación de ASP.NET Core. Leer y escribir datos de forma eficaz es fundamental para un buen rendimiento.
Recomendaciones:
.Where
, .Select
o .Sum
) para que la base de datos realice el filtrado.Los enfoques siguientes pueden mejorar el rendimiento en aplicaciones a gran escala:
Se recomienda medir el impacto de los enfoques anteriores de alto rendimiento antes de confirmar la base de código. Puede que la complejidad adicional de las consultas compiladas no justifique la mejora del rendimiento.
Los problemas de consulta se pueden detectar revisando el tiempo dedicado a acceder a los datos con Application Insights o con herramientas de generación de perfiles. La mayoría de las bases de datos también hacen que las estadísticas estén disponibles para las consultas ejecutadas con frecuencia.
Aunque HttpClient implementa la interfaz IDisposable
, está diseñada para su reutilización. Las instancias cerradas de HttpClient
dejan abiertos los sockets en el estado TIME_WAIT
durante un breve período de tiempo. Si se usa con frecuencia una ruta de acceso al código que crea y elimina objetos HttpClient
, la aplicación puede agotar los sockets disponibles. HttpClientFactory
se introdujo en ASP.NET Core 2.1 como solución a este problema. Controla la agrupación de conexiones HTTP para optimizar el rendimiento y la confiabilidad. Para obtener más información, consulte el artículo sobre el Uso de HttpClientFactory
para implementar solicitudes HTTP resistentes.
Recomendaciones:
HttpClient
directamente.HttpClient
. Para obtener más información, consulte el artículo sobre el uso de HttpClientFactory para implementar solicitudes HTTP resistentes.Quiere que todo el código sea rápido. Las rutas de acceso al código a las que se llama con frecuencia son las más críticas que se deben optimizar. Se incluyen los siguientes:
Recomendaciones:
La mayoría de las solicitudes a una aplicación de ASP.NET Core se pueden controlar mediante un controlador o modelo de página que llama a los servicios necesarios y devuelve una respuesta HTTP. Para algunas solicitudes que implican tareas de larga duración, es mejor que todo el proceso de solicitud-respuesta sea asincrónico.
Recomendaciones:
Las aplicaciones de ASP.NET Core con servidores front-end complejos a menudo proporcionan muchos archivos de JavaScript, CSS o de imagen. El rendimiento de las solicitudes de carga iniciales se puede mejorar mediante:
Recomendaciones:
environment
de ASP.NET Core para controlar los entornos de Development
y Production
.Reducir el tamaño de la respuesta suele aumentar la capacidad de respuesta de una aplicación, a menudo drásticamente. Una manera de reducir el tamaño de la carga útil es comprimir las respuestas de una aplicación. Para obtener más información, vea Compresión de respuesta.
Cada nueva versión de ASP.NET Core incluye mejoras de rendimiento. Las optimizaciones de .NET Core y ASP.NET Core significan que las versiones más recientes suelen superar el rendimiento de las versiones anteriores. Por ejemplo, .NET Core 2.1 agregó compatibilidad con expresiones regulares compiladas y se benefició de Span<T>. ASP.NET Core 2.2 agregó compatibilidad con HTTP/2. ASP.NET Core 3.0 agrega muchas mejoras que reducen el uso de memoria y aumentan el rendimiento. Si el rendimiento es una prioridad, considere la posibilidad de actualizar a la versión actual de ASP.NET Core.
Las excepciones deben ser poco frecuentes. La generación y detección de excepciones es lenta en relación con otros patrones de flujo de código. Por este motivo, las excepciones no se deben usar para controlar el flujo normal del programa.
Recomendaciones:
Las herramientas de diagnóstico de aplicaciones, como Application Insights, pueden ayudar a identificar excepciones comunes en una aplicación que pueden afectar al rendimiento.
Todas las E/S de ASP.NET Core son asincrónicas. Los servidores implementan la interfaz Stream
, que tiene sobrecargas sincrónicas y asincrónicas. Son más convenientes las asincrónicas para evitar bloquear los subprocesos del grupo de subprocesos. El bloqueo de subprocesos puede provocar un colapso del grupo de subprocesos.
No hagas esto: en el ejemplo siguiente se usa ReadToEnd. Bloquea el subproceso actual para esperar el resultado. Este es un ejemplo de resultado sincrónico frente a asincrónico.
public class BadStreamReaderController : Controller
{
[HttpGet("/contoso")]
public ActionResult<ContosoData> Get()
{
var json = new StreamReader(Request.Body).ReadToEnd();
return JsonSerializer.Deserialize<ContosoData>(json);
}
}
En el código anterior, Get
lee sincrónicamente todo el cuerpo de la solicitud HTTP en la memoria. Si el cliente se carga lentamente, la aplicación está realizando una sincronización en lugar de una asincronización. La aplicación realiza la sincronización en lugar de la asincronización porque KestrelNO admite lecturas sincrónicas.
Haz esto: en el ejemplo siguiente se usa ReadToEndAsync y no se bloquea el subproceso mientras se lee.
public class GoodStreamReaderController : Controller
{
[HttpGet("/contoso")]
public async Task<ActionResult<ContosoData>> Get()
{
var json = await new StreamReader(Request.Body).ReadToEndAsync();
return JsonSerializer.Deserialize<ContosoData>(json);
}
}
El código anterior lee sincrónicamente todo el cuerpo de la solicitud HTTP en la memoria.
Aviso
Si la solicitud es grande, la lectura del cuerpo completo de la solicitud HTTP en la memoria podría dar lugar a una condición de memoria insuficiente (OOM). OOM puede dar lugar a una denegación de servicio. Para obtener más información, consulta Evitar la lectura de cuerpos de solicitud grandes o cuerpos de respuesta en memoria en este artículo.
Haz esto: el ejemplo siguiente es totalmente asincrónico mediante un cuerpo de solicitud no almacenado en búfer:
public class GoodStreamReaderController : Controller
{
[HttpGet("/contoso")]
public async Task<ActionResult<ContosoData>> Get()
{
return await JsonSerializer.DeserializeAsync<ContosoData>(Request.Body);
}
}
El código anterior deserializa de forma asincrónica el cuerpo de la solicitud en un objeto de C#.
Usa HttpContext.Request.ReadFormAsync
en lugar de HttpContext.Request.Form
.
HttpContext.Request.Form
solo se puede leer de forma segura con las condiciones siguientes:
ReadFormAsync
yHttpContext.Request.Form
No hagas esto: en el ejemplo siguiente se usa HttpContext.Request.Form
. HttpContext.Request.Form
usa la sincronización en lugar de la asincronización y puede provocar el colapso del grupo de subprocesos.
public class BadReadController : Controller
{
[HttpPost("/form-body")]
public IActionResult Post()
{
var form = HttpContext.Request.Form;
Process(form["id"], form["name"]);
return Accepted();
}
Haz esto: en el ejemplo siguiente se usa HttpContext.Request.ReadFormAsync
para leer el cuerpo del formulario de forma asincrónica.
public class GoodReadController : Controller
{
[HttpPost("/form-body")]
public async Task<IActionResult> Post()
{
var form = await HttpContext.Request.ReadFormAsync();
Process(form["id"], form["name"]);
return Accepted();
}
En .NET, cada asignación de objetos superior a 85 000 KB termina en el montón de objetos grandes (LOH). Los objetos grandes son costosos en dos aspectos:
En esta entrada de blog se describe el problema brevemente:
Cuando se asigna un objeto grande, se marca como objeto Gen 2. No Gen 0 como para objetos pequeños. Las consecuencias son que si se agota la memoria en LOH, GC limpia todo el montón administrado, no solo LOH. Por lo tanto, limpia Gen 0, Gen 1 y Gen 2, incluido LOH. Esto se denomina recolección completa de elementos no utilizados y es la recolección de elementos no utilizados que consume más tiempo. Para muchas aplicaciones, puede ser aceptable. Pero definitivamente no para los servidores web de alto rendimiento, donde se necesitan pocos búferes de memoria grande para controlar una solicitud web media (leer de un socket, descomprimir, descodificar JSON, etc.).
Almacenar un cuerpo de solicitud o respuesta grande en un único byte[]
o string
:
Cuando se usa un serializador/deserializador que solo admite lecturas y escrituras sincrónicas (por ejemplo, Json.NET):
Aviso
Si la solicitud es grande, podría provocar una condición de memoria insuficiente (OOM). OOM puede dar lugar a una denegación de servicio. Para obtener más información, consulta Evitar la lectura de cuerpos de solicitud grandes o cuerpos de respuesta en memoria en este artículo.
ASP.NET Core 3.0 ahora usa System.Text.Json de forma predeterminada para la serialización de JSON. System.Text.Json:
Newtonsoft.Json
.IHttpContextAccessor.HttpContext devuelve el valor HttpContext
de la solicitud activa cuando se obtiene acceso desde el subproceso de solicitud. IHttpContextAccessor.HttpContext
no se debe almacenar en un campo ni en una variable.
No hagas esto: en el ejemplo siguiente se almacena HttpContext
en un campo y, a continuación, se intenta usarlo más adelante.
public class MyBadType
{
private readonly HttpContext _context;
public MyBadType(IHttpContextAccessor accessor)
{
_context = accessor.HttpContext;
}
public void CheckAdmin()
{
if (!_context.User.IsInRole("admin"))
{
throw new UnauthorizedAccessException("The current user isn't an admin");
}
}
}
El código anterior suele capturar un valor HttpContext
null o incorrecto en el constructor.
Haz esto: en el ejemplo siguiente:
HttpContext
en el momento correcto y comprueba null
.public class MyGoodType
{
private readonly IHttpContextAccessor _accessor;
public MyGoodType(IHttpContextAccessor accessor)
{
_accessor = accessor;
}
public void CheckAdmin()
{
var context = _accessor.HttpContext;
if (context != null && !context.User.IsInRole("admin"))
{
throw new UnauthorizedAccessException("The current user isn't an admin");
}
}
}
HttpContext
no es seguro con los subprocesos. El acceso a HttpContext
desde varios subprocesos en paralelo puede dar lugar a un comportamiento inesperado, como que el servidor deje de responder, se bloquee y se dañen los datos.
No hagas esto: en el ejemplo siguiente se realizan tres solicitudes paralelas y se registra la ruta de acceso de la solicitud entrante antes y después de la solicitud HTTP saliente. Se accede a la ruta de acceso de la solicitud desde varios subprocesos, posiblemente en paralelo.
public class AsyncBadSearchController : Controller
{
[HttpGet("/search")]
public async Task<SearchResults> Get(string query)
{
var query1 = SearchAsync(SearchEngine.Google, query);
var query2 = SearchAsync(SearchEngine.Bing, query);
var query3 = SearchAsync(SearchEngine.DuckDuckGo, query);
await Task.WhenAll(query1, query2, query3);
var results1 = await query1;
var results2 = await query2;
var results3 = await query3;
return SearchResults.Combine(results1, results2, results3);
}
private async Task<SearchResults> SearchAsync(SearchEngine engine, string query)
{
var searchResults = _searchService.Empty();
try
{
_logger.LogInformation("Starting search query from {path}.",
HttpContext.Request.Path);
searchResults = _searchService.Search(engine, query);
_logger.LogInformation("Finishing search query from {path}.",
HttpContext.Request.Path);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed query from {path}",
HttpContext.Request.Path);
}
return await searchResults;
}
Haz esto: en el ejemplo siguiente se copian todos los datos de la solicitud entrante antes de realizar las tres solicitudes paralelas.
public class AsyncGoodSearchController : Controller
{
[HttpGet("/search")]
public async Task<SearchResults> Get(string query)
{
string path = HttpContext.Request.Path;
var query1 = SearchAsync(SearchEngine.Google, query,
path);
var query2 = SearchAsync(SearchEngine.Bing, query, path);
var query3 = SearchAsync(SearchEngine.DuckDuckGo, query, path);
await Task.WhenAll(query1, query2, query3);
var results1 = await query1;
var results2 = await query2;
var results3 = await query3;
return SearchResults.Combine(results1, results2, results3);
}
private async Task<SearchResults> SearchAsync(SearchEngine engine, string query,
string path)
{
var searchResults = _searchService.Empty();
try
{
_logger.LogInformation("Starting search query from {path}.",
path);
searchResults = await _searchService.SearchAsync(engine, query);
_logger.LogInformation("Finishing search query from {path}.", path);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed query from {path}", path);
}
return await searchResults;
}
HttpContext
solo es válido siempre que haya una solicitud HTTP activa en la canalización de ASP.NET Core. Toda la canalización de ASP.NET Core es una cadena asincrónica de delegados que ejecuta cada solicitud. Cuando se completa la devolución de Task
desde esta cadena, HttpContext
se recicla.
No hagas esto: en el ejemplo siguiente se usa async void
, que hace que la solicitud HTTP se complete cuando se alcanza el primer valor await
:
async void
SIEMPRE es una práctica incorrecta en las aplicaciones de ASP.NET Core.HttpResponse
una vez completada la solicitud HTTP.public class AsyncBadVoidController : Controller
{
[HttpGet("/async")]
public async void Get()
{
await Task.Delay(1000);
// The following line will crash the process because of writing after the
// response has completed on a background thread. Notice async void Get()
await Response.WriteAsync("Hello World");
}
}
Haz esto: en el ejemplo siguiente se devuelve un objeto Task
al marco de trabajo, por lo que la solicitud HTTP no se completa hasta que se completa la acción.
public class AsyncGoodTaskController : Controller
{
[HttpGet("/async")]
public async Task Get()
{
await Task.Delay(1000);
await Response.WriteAsync("Hello World");
}
}
No hagas esto: en el ejemplo siguiente se muestra un cierre que captura el elemento HttpContext
de la propiedad Controller
. Se trata de una práctica incorrecta porque el elemento de trabajo podría:
HttpContext
incorrecto.[HttpGet("/fire-and-forget-1")]
public IActionResult BadFireAndForget()
{
_ = Task.Run(async () =>
{
await Task.Delay(1000);
var path = HttpContext.Request.Path;
Log(path);
});
return Accepted();
}
Haz esto: en el ejemplo siguiente:
[HttpGet("/fire-and-forget-3")]
public IActionResult GoodFireAndForget()
{
string path = HttpContext.Request.Path;
_ = Task.Run(async () =>
{
await Task.Delay(1000);
Log(path);
});
return Accepted();
}
Las tareas en segundo plano deben implementarse como servicios hospedados. Para más información, consulte Tareas en segundo plano con servicios hospedados.
No haga esto: en el ejemplo siguiente se muestra un cierre que captura el elemento DbContext
del parámetro de acción Controller
. Esto es práctica no conveniente. El elemento de trabajo podría ejecutarse fuera del ámbito de la solicitud. ContosoDbContext
está dentro del ámbito de la solicitud y da como resultado ObjectDisposedException
.
[HttpGet("/fire-and-forget-1")]
public IActionResult FireAndForget1([FromServices]ContosoDbContext context)
{
_ = Task.Run(async () =>
{
await Task.Delay(1000);
context.Contoso.Add(new Contoso());
await context.SaveChangesAsync();
});
return Accepted();
}
Haz esto: en el ejemplo siguiente:
IServiceScopeFactory
es un singleton.ContosoDbContext
desde la solicitud entrante.[HttpGet("/fire-and-forget-3")]
public IActionResult FireAndForget3([FromServices]IServiceScopeFactory
serviceScopeFactory)
{
_ = Task.Run(async () =>
{
await Task.Delay(1000);
await using (var scope = serviceScopeFactory.CreateAsyncScope())
{
var context = scope.ServiceProvider.GetRequiredService<ContosoDbContext>();
context.Contoso.Add(new Contoso());
await context.SaveChangesAsync();
}
});
return Accepted();
}
El código resaltado siguiente:
ContosoDbContext
desde el ámbito correcto.[HttpGet("/fire-and-forget-3")]
public IActionResult FireAndForget3([FromServices]IServiceScopeFactory
serviceScopeFactory)
{
_ = Task.Run(async () =>
{
await Task.Delay(1000);
await using (var scope = serviceScopeFactory.CreateAsyncScope())
{
var context = scope.ServiceProvider.GetRequiredService<ContosoDbContext>();
context.Contoso.Add(new Contoso());
await context.SaveChangesAsync();
}
});
return Accepted();
}
ASP.NET Core no almacena en búfer el cuerpo de la respuesta HTTP. La primera vez que se escribe la respuesta:
No haga esto: el código siguiente intenta agregar encabezados de respuesta una vez iniciada la respuesta:
app.Use(async (context, next) =>
{
await next();
context.Response.Headers["test"] = "test value";
});
En el código anterior, context.Response.Headers["test"] = "test value";
producirá una excepción si next()
se ha escrito en la respuesta.
Haga esto: en el ejemplo siguiente se comprueba si la respuesta HTTP se ha iniciado antes de modificar los encabezados.
app.Use(async (context, next) =>
{
await next();
if (!context.Response.HasStarted)
{
context.Response.Headers["test"] = "test value";
}
});
Haga esto: en el ejemplo siguiente se usa HttpResponse.OnStarting
para establecer los encabezados antes de que los encabezados de respuesta se vacíen en el cliente.
Comprobar si la respuesta no se ha iniciado permite registrar una devolución de llamada que se invocará justo antes de que se escriban los encabezados de respuesta. Comprobación de si la respuesta no se ha iniciado:
app.Use(async (context, next) =>
{
context.Response.OnStarting(() =>
{
context.Response.Headers["someheader"] = "somevalue";
return Task.CompletedTask;
});
await next();
});
Los componentes solo esperan llamarse si es posible que controlen y manipulen la respuesta.
Con el hospedaje en proceso, una aplicación ASP.NET Core se ejecuta en el mismo proceso que su proceso de trabajo de IIS. El hospedaje dentro del proceso proporciona un rendimiento mejor que en el hospedaje fuera del proceso porque las solicitudes no se procesan mediante proxy a través del adaptador de bucle. El adaptador de bucle invertido es una interfaz de red que devuelve el tráfico de red saliente al mismo equipo. IIS controla la administración de procesos con el Servicio de activación de procesos de Windows (WAS).
Los proyectos tienen como valor predeterminado el modelo de hospedaje dentro del proceso en ASP.NET Core 3.0 y versiones posteriores.
Para más información, vea Hospedaje de ASP.NET Core en Windows con IIS.
HttpRequest.ContentLength
es null si el encabezado Content-Length
no se recibe. Null en ese caso significa que no se conoce la longitud del cuerpo de la solicitud; no significa que la longitud sea cero. Dado que todas las comparaciones con null (excepto ==
) devuelven false, la comparación Request.ContentLength > 1024
, por ejemplo, podría devolver false
cuando el tamaño del cuerpo de la solicitud es superior a 1024. No saber esto puede provocar vulnerabilidades de seguridad en las aplicaciones. Es posible que pienses que te estás protegiendo de solicitudes demasiado grandes cuando realmente no es así.
Para obtener más información, consulte la respuesta a StackOverflow.
Para obtener instrucciones sobre cómo crear una aplicación ASP.NET Core confiable, segura, eficiente, comprobable y escalable, consulte patrones de aplicación web empresariales. Hay disponible una aplicación web de ejemplo de calidad de producción completa que implementa los patrones.
Comentarios de ASP.NET Core
ASP.NET Core é un proxecto de código aberto. Selecciona unha ligazón para ofrecer comentarios:
Evento
Campionato do Mundo de Power BI DataViz
Feb 14, 4 PM - Mar 31, 4 PM
Con 4 posibilidades de entrar, poderías gañar un paquete de conferencias e facelo ao Live Grand Finale en Las Vegas
Máis información