Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 16 de mayo de 2017
ASP.NET CoreA la hora de iniciar un nuevo proyecto ASP.NET Core, una de las primeras decisiones que debemos tomar es si el target de éste será .NET Core o .NET Framework.

Muchas veces esto dependerá de los requisitos y el entorno del proyecto; por ejemplo, si nos interesa la capacidad para ejecutarlo o desarrollarlo sobre entornos Linux o Mac, nos tendremos que decantar por .NET Core, pues el framework completo sólo está disponible para Windows. También podemos encontrarnos con que necesitamos (re)utilizar componentes o bibliotecas que aún no han sido portadas a .NET Core, por lo que en este caso el target será .NET Framework (bueno, esto cambiará bastante con la llegada de Net Standard 2.0, pero de momento es lo que hay).

En cualquier caso, la decisión la tomamos justo en el momento de crear el proyecto en Visual Studio, al seleccionar la plantilla que usaremos como base:

Cuadro de diálogo de creación de proyecto ASP.NET Core en Visual Studio 2017

Sin embargo, conforme el proyecto avanza, puede que esta decisión que tomamos tan al principio no sea del todo válida: quizás en su momento elegimos .NET Core, pero ahora debemos cambiar a .NET Framework. O al contrario, porque ahora necesitamos que nuestra aplicación sea multiplataforma. O tal vez necesitemos las dos cosas al mismo tiempo por si acaso…

Nota: aunque aún pululan por ahí aplicaciones creadas con versiones preliminares del SDK, basadas en el difunto project.json, aquí utilizaremos la versión 1.0 del SDK, que ya utiliza el nuevo .csproj. Si todavía no has migrado, ya estás tardando ;)

1. Cambiar de target .NET Core a .NET Framework, o viceversa

Imaginemos un proyecto creado inicialmente para .NET Core. Si usamos Visual Studio 2017, veremos en el explorador de soluciones que las dependencias están divididas en dos categorías: NuGet y SDK. En la primera encontraremos referencias hacia los paquetes requeridos por el proyecto, mientras que en la segunda se hace referencia al runtime y bibliotecas de .NET Core, incluidas en Microsoft.NETCore.App:

Carpeta "Dependencies" de un proyecto targeting .NET Core en el explorador de soluciones de Visual Studio

En este caso, en el archivo de proyecto TuAplicacion.csproj encontraremos algo como lo siguiente (los números de versiones mostrados pueden variar dependiendo de la versión de .NET Core y ASP.NET Core que estemos utilizando):
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp1.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore" Version="1.1.1" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.1.2" />
    <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="1.1.1" />
    <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="1.1.1" />
    <PackageReference Include="Microsoft.VisualStudio.Web.BrowserLink" Version="1.1.0" />
  </ItemGroup>

</Project>
Como se puede intuir, el retargeting a .NET Framework básicamente consiste en modificar el target framework moniker (TFM, o identificador de framework) "netcoreapp1.0" que se especifica en la etiqueta <TargetFramework> y sustituirlo por el identificador de la versión del framework .NET que vayamos a utilizar (podéis ver una lista de opciones aquí).

Por ejemplo, si modificamos de la siguiente forma el archivo .csproj indicaremos que el target de nuestro proyecto será .NET 4.6.1 usando el TFM "net461":
<Project Sdk="Microsoft.NET.Sdk.Web">

    <PropertyGroup>
        <TargetFramework>net461</TargetFramework>
    </PropertyGroup>
    
    ...
</Project>
Obviamente, podríamos utilizar otros TFM como "net462" o "net47" (para la última versión del framework). El único requisito es que lo tengamos instalado en nuestro equipo porque de lo contrario aparecerá un error como el siguiente cuando se intenten restaurar las dependencias.:
Error MSB3644: The reference assemblies for framework ".NETFramework,Version=v4.7" were not found. To resolve this, install the SDK or Targeting Pack for this framework version or retarget your application to a version of the framework for which you have the SDK or Targeting Pack installed. Note that assemblies will be resolved from the Global Assembly Cache (GAC) and will be used in place of reference assemblies. Therefore your assembly may not be correctly targeted for the framework you intend.
Al hacer este cambio el proyecto ya no requeriría el uso de .NET Core, por lo que desaparecerá la referencia al paquete Microsoft.NETCore.App, siendo sustituida por referencias a los ensamblados requeridos del framework .NET:



Ya hemos visto cómo modificar el target del proyecto desde .NET Core a .NET framework, pero, ¿y si queremos hacerlo justo al contrario? Pues como seguro podréis adivinar, si queremos cambiar de .NET Framework a .NET Core, el cambio consistirá en establecer el tag <TargetFramework> apropiadamente, por ejemplo usando el TFM "netcoreapp1.0" o el correspondiente a la versión de .NET Core que tengamos instalada en nuestro equipo.

2. Multitargeting: proyectos para .NET Core y .NET Framework al mismo tiempo

Lo que hemos visto anteriormente está bien si nos interesa que nuestro proyecto tenga un único target, es decir, que esté preparado para ejecutarse bien en .NET Core o bien .NET framework, pero no ambas cosas a la vez. Si es eso lo que nos interesa, estaríamos hablando de multitargeting, es decir, la capacidad de un proyecto de compilarse y publicarse para más de un framework de forma simultánea.

Para conseguir esto, en primer lugar debemos retocar ligeramente el archivo de proyecto .csproj, sustituyendo la etiqueta <TargetFramework> por <TargetFrameworks> (ojo al plural) e introduciendo los distintos TFM separados por punto y coma:
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFrameworks>netcoreapp1.0;net461</TargetFrameworks>
  </PropertyGroup>

  ...

</Project>
Carpeta de binarios en proyectos multi targetA partir de que configuremos el proyecto como multi target, cada compilación generará en la carpeta /bin los binarios correspondientes a cada uno de los frameworks especificados, es decir, se irán compilando al mismo tiempo todos ellos.

Además, nuestro proyecto se comportará de forma diferente en algunos aspectos. Por ejemplo, a la hora de publicarlo, obligatoriamente deberemos indicar el framework destino de dicha publicación, pues de lo contrario fallará el proceso:

Selección de target al publicar un proyecto
De la misma forma, deberemos indicar obligatoriamente el target al hacer la publicación usando la línea de comandos:

C:\MyProject>dotnet publish
Microsoft (R) Build Engine version 15.1.1012.6693
Copyright (C) Microsoft Corporation. All rights reserved.

Error: The 'Publish' target is not supported without specifying a target framework.
The current project targets multiple frameworks, please specify the framework for 
the published application.

C:\MyProject>dotnet publish -f net461
Microsoft (R) Build Engine version 15.1.1012.6693
Copyright (C) Microsoft Corporation. All rights reserved.

MyProject -> c:\MyProject\bin\Debug\net461\MyProject.exe

C:\MyProject>_

Otro aspecto importante que cambiará es la gestión de dependencias. Cuando tenemos un target único y añadimos una referencia al proyecto, ésta es chequeada para comprobar que es compatible.
Por ejemplo, si en un proyecto para .NET Core intentamos añadir una referencia hacia el paquete NuGet "iTextSharp" (un componente de generación de PDF sólo disponible para .NET Framework), se producirá un error al restaurarlo:
Package iTextSharp 5.5.11 is not compatible with netcoreapp1.0 (.NETCoreApp,Version=v1.0). Package iTextSharp 5.5.11 supports: net(.NETFramework,Version=v0.0)
One or more packages are incompatible with .NETCoreApp,Version=v1.0.
Sin embargo, cuando el proyecto tiene múltiples targets podemos referenciar paquetes asociándolos a frameworks específicos. Como muestra, en el siguiente código se puede observar cómo añadimos el paquete "iTextSharp" sólo para el target "net461":
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFrameworks>netcoreapp1.0;net461</TargetFrameworks>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore" Version="1.0.4" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.0.3" />
    ...
  </ItemGroup>

  <ItemGroup Condition="'$(TargetFramework)' == 'net461'">
    <PackageReference Include="iTextSharp" Version="5.5.11" />
  </ItemGroup>
  ...

</Project>
Es decir, cuando estemos publicando o compilando para .NET 4.6.1, se incluirán las referencias correspondientes al paquete "iTextSharp" y podremos utilizarlo desde nuestro código.
Sin embargo, veréis que hay un problema en este enfoque. Si un proyecto es multi-target, quiere decir que la misma base de código se utilizará para generar los binarios destinados a los distintos frameworks. Y si existen dependencias hacia paquetes incompatibles, ¿cómo se gestiona esto?

Pues en primer lugar, hay que tener en cuenta que el uso desde código de cualquier biblioteca incompatible con algunos de los frameworks de destino generará un error de compilación. Por ejemplo, si incluimos una línea de código como la siguiente, donde intentamos crear un objeto propio de "iTextSharp", fallará la compilación:
var x = new iTextSharp.text.pdf.PdfDocument();
Error CS0246: The type or namespace name 'iTextSharp' could not be found (are you missing a using directive or an assembly reference?)
La verdad es que el compilador podría ser algo más explícito, pero el caso es que la compilación está fallando sólo para el target .NET Core, puesto que en este framework el paquete "iTextSharp" no ha sido incluido y, por tanto, el tipo PdfDocument no está disponible.

A nivel de IDE, Visual Studio 2017 nos ayudará a saber cuándo estamos introduciendo código no compatible con algunos de los targets. De hecho, en la propia ventana de edición del entorno existe un desplegable en el que podremos seleccionar el target a tener en cuenta durante la edición. En la siguiente captura de pantalla se muestra este desplegable, en el que tenemos actualmente seleccionado "netcoreapp1.1", por lo que vemos que se marca como error el intento de uso del espacio de nombres iTextSharp:

Desplegable de selección del target

En cambio, si en el desplegable indicamos que queremos editar en el contexto del target .NET 4.6.1 ya el error no aparecerá, aunque aún tendremos disponible información sobre la compatibilidad de estas líneas en los distintos targets:

Ayuda contextual indicando la compatibilidad de un tipo con los distintos targets
Para solucionar los problemas en compilación debemos introducir código condicional, es decir, código que sólo se introduzca en el proyecto en un target determinado. Algo parecido a lo que hicimos anteriormente al referenciar el paquete NuGet, pero utilizando la directiva #if de C# para comprobar si existen constantes como NET461 o NETCOREAPP1_1.

Por ejemplo, el siguiente bloque de código muestra un controlador MVC que podría retornar un archivo PDF o TXT dependiendo de si el target es .NET 4.6.1 (que es compatible con "iTextSharp") o no. Si el target es .NET Core, no compatible con el paquete necesario para generar PDF, proporcionamos al menos una alternativa honrosa retornando el archivo en formato de texto plano:
public IActionResult DownloadInvoiceDocument(int invoiceId)
{
    var invoice = _invoiceServices.GetInvoice(invoiceId);
    if (invoice == null)
    {
        return NotFound();
    }

    #if NET461
        return File(TextSharpHelper.GenerateInvoicePdf(invoice), "application/pdf");
    #else
        return File(PlainTextHelper.GenerateInvoiceText(invoice), "text/plain");
    #endif
}
...

#if NET461
    public class TextSharpHelper
    {
        internal static string GenerateInvoicePdf(dynamic invoice)
        {
            // Generate PDF using iTextSharp
        }
    }
#endif
Ya habiendo tomado estas precauciones, el proyecto compilará para todos los targets, aunque en cada uno incluirá distintas funcionalidades.

Publicado en Variable not found.

Aún no hay comentarios, ¡sé el primero!