Ya hace tiempo que se lanzó .NET 5, pero seguro que algunos os habéis dado cuenta de que cuando desde Visual Studio creamos determinados tipos de proyecto, como bibliotecas de clases o proyectos de consola, por defecto éstos utilizan como target .NET Core 3.1 en lugar de .NET 5.
No se trata de un error; desde Microsoft justifican esta decisión porque .NET 5 no es una versión LTS, y prefieren que por defecto los proyectos sean creados usando una versión con mayor tiempo de soporte, como .NET Core 3.1.
Esto tiene fácil solución, porque tras crearlo simplemente deberíamos acceder a las propiedades del proyecto o editar el archivo .csproj
y modificar ajustar el target framework a nuestro antojo, pero, ¿cómo podríamos evitar tener que hacer esto cada vez?
En Blazor, es habitual implementar la interfaz de nuestros componentes de forma declarativa, mezclando etiquetas que:
- Pueden ser HTML, y cuyo resultado irá textualmente al navegador
- Pueden ser instancias de otros componentes, por lo que al navegador irá el resultado de la renderización de éstos.
Por ejemplo, el siguiente código envía al navegador la etiqueta <h1>
y luego el resultado de renderizar el componente Blazor <MyComponent>
, en cuyo interior podrán existir a su vez otras etiquetas o componentes:
@page "/home"
<h1>Welcome!<h1>
<MyComponent />
Fijaos que, aunque es la fórmula que emplearemos en la mayoría de ocasiones, es una estructura totalmente rígida: podemos tener la total certeza de que el componente que será instanciado en la página anterior es MyComponent
, pero, ¿qué ocurre si queremos poder decidir en tiempo de ejecución qué componente instanciaremos en la página?
Todos los usuarios que están utilizando una aplicación Blazor Server mantienen una conexión abierta con el servidor para enviar eventos y recibir las modificaciones de la interfaz de usuario. Y si habéis trabajado algo con Blazor, sabréis que cuando dicha conexión se corta aparece en el navegador un mensaje como el mostrado en la siguiente captura de pantalla:
Como se puede ver en la captura anterior, cuando se detecta la desconexión, se añade automáticamente a la página un <div>
con el identificador components-reconnect-modal
que bloquea la página (observad su posición fija a pantalla completa), mostrando al usuario un mensaje informándole de que se está intentando reconectar.
Si, transcurridos algo más de 30 segundos, la reconexión no ha sido posible, el contenido del <div>
cambiará para informar al usuario de que la conexión no pudo ser restablecida, y ofreciendo un botón para reintentarlo y un enlace para recargar la página completa:
Podemos comprobar muy fácilmente estos comportamientos si lanzamos la aplicación sin depuración (Ctrl+F5) desde Visual Studio y detenemos IIS Express desde su icono en la barra de herramientas de Windows.
Como comportamiento por defecto la verdad es que no está nada mal, pues nos proporciona una solución out of the box que será suficiente la mayoría de los casos. Sin embargo, la estética es obviamente mejorable... ¿y si quisiéramos modificar visualmente estos mensajes o incluso su comportamiento? Pues es lo que veremos en este post 🙂
Como sabemos, al acceder por primera vez a una aplicación Blazor WebAssembly, durante unos segundos aparecerá en el navegador el mensaje "Loading...", indicando que se están descargando los recursos necesarios para que funcione la aplicación.
Vimos también en su momento que este mensaje puede ser modificado a nuestro antojo para sustituirlo por algo más apropiado, como un espectacular spinner:
Sin embargo, si el navegador desde el que estamos accediendo es un vetusto Internet Explorer u otro sin soporte para Blazor WebAssembly, el mensaje de carga se mostrará indefinidamente en el navegador. No se mostrarán errores, ni siquiera por la consola, por lo que el usuario se quedará esperando sin saber que, en realidad, la aplicación no cargará jamás.
Lo que vamos a ver en este post es cómo detectar este escenario e informar al usuario de que para acceder a la aplicación debe usar un browser más moderno.
Va un post rapidito, pero que seguro que puede ahorrar quebraderos de cabeza a más de uno que se encuentre con este problema al iniciar desde Visual Studio una aplicación ASP.NET, ASP.NET Core MVC/Web API, Razor Pages, o incluso Blazor que utilicen por debajo IIS Express.
El problema ocurre justo al ejecutar la aplicación con F5 o Ctrl+F5 desde el entorno de desarrollo; en ese momento, la ejecución se detiene y aparece un cuadro de diálogo con el mensaje:
"Unable to connect to web server 'IIS Express'"
Creo que llevo años encontrándome de vez en cuando con este problema al arrancar las aplicaciones, y nunca entendí muy bien por qué pasaba. Buscaba por la red y solo encontraba soluciones relativas a eliminar el archivo de configuración applicationhost.config
que Visual Studio guarda en la carpeta ".vs" de la solución, a reiniciar el IDE o incluso la máquina, abrirlo como Administrador, o cambiar el puerto en la configuración del proyecto, normalmente algunas unidades por arriba o por abajo que el puerto asignado inicialmente.
Esta última opción es la que más veces me ha funcionado, pero no siempre iba bien, por lo que al final tampoco la identifiqué como una clara receta para paliar el problema que nos ocupa.
Hace unos días me ha vuelto a ocurrir con un proyecto que uso muy a menudo, y, de un día para otro, ha dejado de funcionar y ha comenzado a lanzarme a la cara el maldito cuadro de diálogo. La diferencia es que por fin he podido encontrar una respuesta satisfactoria, al menos para alguno de los escenarios que pueden causar el error :)
Veamos cómo.
Por ejemplo, el siguiente código en el interior de un componente definiría un par de propiedades llamadas
Text
y Repeat
, que utiliza para repetir un texto tantas veces como se le indique:@* File: Repeater.razor *@
@for (var i = 0; i < Repeat; i++)
{
<p>@Text</p>
}
@code {
[Parameter]
public string Text { get; set; }
[Parameter]
public int Repeat { get; set; }
}
De esta forma, podríamos utilizar el componente desde otros puntos de la aplicación suministrándole los valores necesarios en cada caso:<Repeater Text="Hola" Repeat="6" />
Sin embargo, alguna vez podría resultar interesante poder obtener todos los parámetros que se han enviado a un componente, pero sin necesidad de conocerlos previamente.Por ejemplo, considerad el siguiente código en un componente Blazor:
<p>This text is encoded: @myHtml</p>
@code {
string myHtml = "Hello, <b>this is bold</b>";
}
El resultado que enviaremos al navegador es el siguiente:<p>This text is encoded: Hello, <b>this is bold</b></p>
Y, por tanto, nuestros usuarios podrán leer literalmente este párrafo:This text is encoded: Hello, <b>this is bold</b>
Normalmente no es eso lo que queremos mostrarles, ¿verdad?@code
en el mismo archivo .razor
, como en el siguiente bloque de código:@* File: ~/Pages/HelloWorld.razor *@
@page "/hello"
<h1>Hello</h1>
<div>
<label for="name">Your name:</label>
<input id="name" @bind-value="Name" @bind-value:event="oninput"/>
</div>
@if (HasName)
{
<h2>Hello, @Name!</h2>
}
@code
{
public string Name { get; set; }
public bool HasName => !string.IsNullOrWhiteSpace(Name);
}
Sin embargo, esto no tiene que ser necesariamente así. En tiempo de compilación, un archivo .razor
genera una clase parcial C# con su mismo nombre, lo cual nos brinda la posibilidad de mover todo el código C# a otra porción de dicha clase.Podéis ver las clases generadas para cada archivo.razor
en la carpeta del proyectoobj\debug\netcoreapp3.1\Razor\Pages
.
El estado actual de acumulación de SDKs podéis conocerlo muy fácilmente desde línea de comandos utilizando la orden
dotnet --list-sdks
:C:\>dotnet --list-sdks
1.1.11 [C:\Program Files\dotnet\sdk]
2.1.202 [C:\Program Files\dotnet\sdk]
2.1.503 [C:\Program Files\dotnet\sdk]
2.1.504 [C:\Program Files\dotnet\sdk]
2.1.505 [C:\Program Files\dotnet\sdk]
2.1.507 [C:\Program Files\dotnet\sdk]
2.1.508 [C:\Program Files\dotnet\sdk]
2.1.509 [C:\Program Files\dotnet\sdk]
2.1.511 [C:\Program Files\dotnet\sdk]
2.1.602 [C:\Program Files\dotnet\sdk]
2.1.604 [C:\Program Files\dotnet\sdk]
2.1.700 [C:\Program Files\dotnet\sdk]
2.1.701 [C:\Program Files\dotnet\sdk]
2.1.800-preview-009696 [C:\Program Files\dotnet\sdk]
2.1.801 [C:\Program Files\dotnet\sdk]
2.1.802 [C:\Program Files\dotnet\sdk]
2.2.102 [C:\Program Files\dotnet\sdk]
2.2.105 [C:\Program Files\dotnet\sdk]
2.2.202 [C:\Program Files\dotnet\sdk]
2.2.203 [C:\Program Files\dotnet\sdk]
2.2.204 [C:\Program Files\dotnet\sdk]
3.0.100-preview8-013656 [C:\Program Files\dotnet\sdk]
3.0.100-preview9-014004 [C:\Program Files\dotnet\sdk]
3.0.100 [C:\Program Files\dotnet\sdk]
3.1.100 [C:\Program Files\dotnet\sdk]
3.1.101 [C:\Program Files\dotnet\sdk]
3.1.102 [C:\Program Files\dotnet\sdk]
C:\>_
En mi caso, tengo aún por ahí el SDK de .NET Core 1.x, un par de decenas de .NET Core 2.x, y algunas previews, pero los he visto mucho peores ;) Obviamente, salvo dos o tres versiones que quizás me interesen porque tengo aplicaciones que aún no he migrado, el resto ocupan en mi equipo un espacio considerable sin motivo, más de 5GB, pues cada SDK puede pesar entre 150 y 200 Mb.
Personalmente me gusta tener todos los entornos y herramientas de desarrollo en inglés, básicamente porque cuando encontramos problemas es más fácil encontrar soluciones si a la hora de buscar utilizamos los términos en este idioma... bueno, y de paso, evito ver algunas traducciones terribles ;)
Pues bien, en el caso del SDK de .NET Core, el idioma no es una característica que podamos elegir a la hora de instalarlo. Se instalarán todos los idiomas disponibles (podéis verlo por ejemplo en la carpeta
%programfiles%\dotnet\sdk\3.1.101
), y los mensajes se mostrarán en el idioma configurado por defecto en nuestra máquina. En mi equipo, por ejemplo, se muestra todo en idioma español:C:\>dotnet xyz
No se pudo ejecutar porque no se encontró el comando o archivo especificados.
Algunas de las posibles causas son:
* Escribió mal un comando dotnet integrado.
* Intentó ejecutar un programa .NET Core, pero dotnet-xyz no existe.
* Tiene planeado ejecutar una herramienta global, pero no se encontró un ejecutable
con prefijo dotnet con este nombre en la RUTA DE ACCESO.
C:\>_
El hecho de que en el equipo destino esté preinstalado el runtime es muy interesante, entre otras cosas porque permite asegurar de antemano que en él se encontrarán todas las dependencias (frameworks, bibliotecas, paquetes, metapaquetes) necesarios para una correcta ejecución. Por tanto, para distribuir nuestra aplicación sólo debemos generar lo relativo a nuestro código, el resto ya estará allí.
Esta forma de publicar aplicaciones se denomina framework-dependent, pues dependen de que determinados componentes del framework estén instalado en el destino.Por ejemplo, el paquete de publicación de una aplicación de consola prácticamente vacía, que únicamente muestra el mensaje "Hello world!", ocuparía solo 175K:
D:\MyConsoleApp\output>dir
El volumen de la unidad D es Datos
El número de serie del volumen es: 8CBC-81E3
Directorio de D:\MyConsoleApp\output
09/02/2020 18:47 <DIR> .
09/02/2020 18:47 <DIR> ..
09/02/2020 18:46 428 MyConsoleApp.deps.json
09/02/2020 18:46 4.608 MyConsoleApp.dll
09/02/2020 18:46 169.984 MyConsoleApp.exe
09/02/2020 18:46 668 MyConsoleApp.pdb
09/02/2020 18:46 154 MyConsoleApp.runtimeconfig.json
5 archivos 175.842 bytes
2 dirs 463.058.874.368 bytes libres
D:\MyConsoleApp\output>_
Otra ventaja de este tipo de distribución es que es cross platform pura, es decir, podemos copiar los archivos a cualquiera de los sistemas operativos soportados y, siempre que dispongan del runtime, nuestra aplicación podrá correr sobre ellos sin problema.Y todo esto está muy bien, pero, ¿qué pasa si quiero crear una aplicación portable, de forma que pueda distribuirla y ejecutarla sin necesidad de que el equipo destino tenga nada preinstalado?
Pues eso es lo que veremos en este post ;)
Pues bien, esto ha cambiado a partir de la versión 3.0, en mi opinión, hacia una dirección bastante más correcta. Veamos en qué consisten estos cambios.
Pues bien, tras años sufriendo esto en silencio, he decidido invertir unos minutos a ver si existía una forma de ahorrarme los dichosos segundos que tardaba en desactivar la captura de tráfico cada vez que abría la herramienta.
Y como efectivamente es posible, os dejo la forma de conseguirlo por si hay por ahí algún perezoso más al que pueda interesarle ;)
Error en proyectos gRPC al usar un nombre de usuario de Windows con espacios o caracteres especiales
C:\Users\<Tu nombre>
:De hecho, llevo ya varios encontronazos de este tipo en los últimos meses (por ejemplo, con Anaconda o con el SDK de Android para Windows), y el último lo he tenido al intentar utilizar las herramientas de Protobuf para la creación de servicios gRPC que, como sabéis, es una de las novedades incluidas en .NET Core 3.
Aunque el problema lo he encontrado trabajando con Visual Studio, ciertamente este IDE tiene poco que decir al respecto; ocurre lo mismo usando línea de comandos o cualquier otro entorno que se base en el tooling oficial de gRPC para .NET.
TagWith()
de Entity Framework Core que, como recordaréis, venía de perlas para incluir un comentario en las consultas SQL enviadas al servidor de datos para poder identificarlas a posteriori:LINQ
============================================
var query = context.Friends
.OrderByDescending(friend => friend.Age)
.Take(10)
.Select(friend =>
new { FriendName = friend.Name,
friend.Age,
CountryName = friend.Country.Name
})
.TagWith("Get top 10 older friends with country");
Generated SQL
============================================
-- Get top 10 older friends with country
SELECT TOP(@__p_0) [friend].[Name] AS [FriendName], [friend].[Age],
[friend.Country].[Name] AS [CountryName]
FROM [Friends] AS [friend]
LEFT JOIN [Countries] AS [friend.Country]
ON [friend].[CountryId] = [friend.Country].[Id]
ORDER BY [friend].[Age] DESC
Mientras escribía el post, pensaba que quizás sería interesante poder utilizar esta misma idea en Entity Framework 6, porque, al fin y al cabo, todos tenemos por ahí muchas aplicaciones en producción que continúan delegando en este marco de trabajo su acceso a datos. Tras una búsqueda en Google y no encontrar gran cosa, pensé que tampoco sería tan difícil de implementar algo que diera el apaño...TL;DR: Echad un vistazo a EF6.TagWith en NuGet o en el repo de GitHub.
Esta capacidad abre posibilidades bastante interesantes,que no eran tan inmediatas (o en algunos casos, ni siquiera posibles de forma directa) en versiones "clásicas" de Entity Framework, o EF Core anterior a 2.1. Gracias a ella podremos, por ejemplo, tener en nuestra entidad una propiedad de tipo enum mapeada a una cadena de caracteres en el almacén de datos, o introducir cualquier lógica de transformación, como podría ser la desencriptación y encriptación de valores, a la hora de leer y persistir información.
En versiones anteriores a EF Core 2.1 y todos sus antecesores "clásicos", un problema que era difícilmente salvable era la necesidad de que en las entidades existiera un constructor público sin parámetros.
Esto tenía sentido, pues debíamos proporcionar al framework una vía para crear las instancias al materializarlas desde el almacén de datos. De hecho, al materializar, el marco de trabajo usaba dicho constructor para crear la instancia, y luego iba poblando las propiedades una a una utilizando sus setters (o estableciendo el valor de sus backing fields, como vimos en un post anterior)
Ya llevamos varios posts dedicados a comentar algunas características novedosas o que cambian bastante respecto a las versiones anteriores (como la evaluación en cliente o las shadow properties, y vamos a continuar en esta línea presentando ahora otra interesante novedad: el soporte para campos de respaldo o backing fields.
Por ejemplo, si instalamos en nuestro equipo la preview del SDK 3.0, a partir de ese momento todos los comandos de la CLI se ejecutarán utilizando esta versión preliminar, como cuando creamos un nuevo proyecto usando
dotnet new
; en este caso, el proyecto se construirá usando la plantilla proporcionada por la versión más actual del framework (aunque sea preview), lo cual puede resultar molesto en algunas ocasiones.En este post vamos a ver cómo el propio SDK dispone de mecanismos que nos permiten seleccionar una versión específica, para lo cual trataremos varios comandos útiles de la CLI.