Envío de mail con framework 2.0, mi clase con autenticación

30 10 2007

A todos nos llega en algún momento la necesidad de que nuestro sistema envíe mails. Es ahí donde donde toda nuestra pasión por la programación se reduce a google y “ojala que alguién lo tenga resuelto”.

No es difícil encontrar como enviar mails con .Net, pero luego nos encontramos con diferentes requerimientos de formato, seguridad, etc. y como no queremos interiorizarnos en como MS implementó el aburrido protocolo SMTP en el framework, recurrimos nuevamente a google para aburrirnos un rato más. Bueno, he aquí el producto de mi tiempo desperdiciado y algunos comentarios sobre el tema. Si solamente necesitás una clase para enviar mails sin mucha vuelta te recomiendo ir directamente al código y aplicar un “copy-paste”, ya que el código es bastante auto-explicativo y hay un solo método para utilizar.

El namespace utilizado es System.Net.Mail (solo framework 2.0, antes era System.Web.Mail)

Basicamente, lo que hacemos es:

  • Crear un objeto MailMessage
  • Crear y asignar a la propiedad “From” del objeto MailMessage un objeto MailAddress con la dirección de salida
  • Crear y asignar a la propiedad “To” del objeto MailMessage una colección de objetos MailAddress conteniendo las direcciones de los destinatarios
  • Asignar un texto a las propiedades “Subject” y Body” del objeto MailMessage
  • Crear un objeto SmtpClient, definirle la dirección del host y el puerto, e invocar a su método Send pasándole como parámetro nuestro MailMessage

Esa es la manera de enviar un mail simple, con un body de texto plano, sin attachments y por el puerto 25 (default de SMTP), sin embargo, el único método público de mi clase (Enviar) tiene una serie de parámetros opcionales (serán sobrecargas para quien lo quiera pasar a C#) que permiten utilizar otras características:

MailPort permite especificar la utilización de otro puerto para conectarse al host SMTP (por ejemplo 443 para utilizar ssl), altarando la propiedad Port del SmtpClient .

MailIsBodyHtml permite especificar si el texto enviado es Html o texto plano (propiedad IsBodyHtml del objeto MailMessage).

EnableSSL determina si se utiliza (1), o no (0) este protocolo en la conexión con el host (propiedad EnableSsl de SmtpClient).

SSLuser y SSLpass permite enviar al host un user y password para establecer la conexión (propiedad Credentials de SmtpClient).

En el código no manejo attachments, pero para quien deba hacerlo, estos se incluyen por medio instancias de la clase System.Net.Attachment, que permite especificar un path o un stream como origen, el tipo de contenido (su mimetype) y algunas propiedades más. Estos objetos se adjuntan mediante el método Add de la colección Attachments del objeto MailMessage. Cuando pueda probar la clase con archivos adjuntos en un ambiente real, actualizo el código comentando si hubo algún issue al respecto.

Respecto al body en formato html, se puede comentar, aunque hablamos de SMTP y no de .Net, que si incluimos imágenes, nuestro html puede hacer referencia a imágenes hosteadas en algún server o a imágenes adjuntas, en cuyo caso se incluye solo el nombre de la imágen en el tag img.

Autenticación en un host con ssl

Donde hay muchas veces un gran problema, es en el uso de SSL para conectarse al host SMTP. Ocurre que para establecer el canal SSL el host utiliza un certificado, nuestra máquina cliente intentará validar ese certificado contra las autoridades certificantes registradas un su propio repositorio, y si no lo consigue arrojará una excepción con el fatídico texto “The underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel” y dentro de ella otra con el más aclaratorio: “The remote certificate is invalid according to the validation procedure“. Esto generalmente ocurre porque el certificado no está emitido por una autoridad (CA) reconocida en nuestra máquina cliente, entonces podemos:

  • Obtener el certificado root de la CA e instalarlo la máquina donde correrá nuestro sistema para que esta reconozca el certificado del host SMTP (esto no pasa mucho más lejos de un click derecho en el archivo del certificado), normalmente le pediremos el mismo al administrador del host.
  • Solicitarle al responsable del host que pague un certificado de una CA reconocida, digamos VeriSign, etc. (esto siempre por teléfono o mail, nunca personalmente y si es el caso preparar antes un CV, por si nos quedamos sin cliente por insolentes)
  • Considerar que lo unico que nos interesa es que nuestro sistema envíe los condenados mails y forzar al framework a realizar una validación personalizada o a aceptar como válido cualquier certificado que el host posea (está última la opción del ejemplo)

Para ello (la tercera opción) el framework 2.0 nos expone una propiedad del objeto ServicePointManager, ServerCertificateValidationCallback, que nos permite especificar un delegado a una función que se encargue de validar el certificado a nuestro gusto, por ejemplo, retornando siempre true. Si quisieramos realizar una validación personalizada, como chequear el DN del certificado, deberíamos hacerlo aquí, ya que nuestro método de validación recibe el certificado en un parámetro, permitiendos acceder a todas sus propiedades.

Es importante destacar que esto último se aplica también a cualquier conexión http vía ssl realizada por el framework, llámese Web Service o HttpWebRequest.

Por último aquí un sitio dedicado al namespace System.Net.Mail y, si necesitamos tocar los headers del mensaje, este artículo de Scott Mitchell (cuando no) desde donde podemos acceder también a otros artículos de su autoría sobre envío de mails con .Net en sus diferente versiones.

Public Class Correo


    Public Shared Sub Enviar(ByVal MailDestinatarios As System.Net.Mail.MailAddressCollection, _
    ByVal MailDireccionOrigen As String, _
    ByVal MailNombreOrigen As String, _
    ByVal MailSubject As String, _
    ByVal MailBody As String, _
    ByVal MailHost As String, _
    Optional ByVal MailPort As Integer = 25, _
    Optional ByVal MailIsBodyHtml As Boolean = True, _
    Optional ByVal EnableSSL As Integer = 0, _
    Optional ByVal SSLuser As String = "", _
    Optional ByVal SSLpass As String = "")

        'Se crea el mensaje
        Dim oMensaje As New MailMessage

        'Se crea el cliente SMTP
        Dim oSMTP As New SmtpClient()

        'Se agregan las direcciones
        oMensaje.From = New MailAddress(MailDireccionOrigen, MailNombreOrigen)
        For Each addr As MailAddress In MailDestinatarios
            oMensaje.To.Add(addr)
        Next

        oMensaje.Subject = MailSubject
        oMensaje.Body = MailBody
        oMensaje.IsBodyHtml = MailIsBodyHtml
        'Seteo que el server notifique solamente en el error de entrega
        oMensaje.DeliveryNotificationOptions = DeliveryNotificationOptions.OnFailure

        'servidor smtp (dirección IP ó nombre de host)
        oSMTP.Host = MailHost
        'Puerto a utilizar (25 por defecto)
        oSMTP.Port = MailPort

        'Si se solicitó SSL, lo activo
        If EnableSSL = 1 Then
            oSMTP.EnableSsl = True
            'Bypass de validación de certificado (para problemas con servidores de SMTP con SSL con certificados que no validan en nuestra máquina)
            ServicePointManager.ServerCertificateValidationCallback = New RemoteCertificateValidationCallback(AddressOf ValidarCertificado)
        End If
        'Cargo las credenciales si hacen falta
        If Not String.IsNullOrEmpty(SSLuser) Then
            Dim credenciales As New System.Net.NetworkCredential(SSLuser, SSLpass)
            oSMTP.Credentials = credenciales
        End If

        Try
            oSMTP.Send(oMensaje)
        Catch ex As SmtpException
            Throw 'Aqui rutina de comprobación propia y logueo de excepción
        Catch ex As Exception
            Throw 'Aqui logueo de excepción
        Finally
            oMensaje = Nothing
            oSMTP = Nothing
        End Try
    End Sub


    Private Shared Function ValidarCertificado(ByVal sender As Object, ByVal certificate As X509Certificate, ByVal chain As X509Chain, ByVal sslPolicyErrors As System.Net.Security.SslPolicyErrors) As Boolean
        'bypass de la validación del certificado (aplicar aquí validación personalizada si fuera el caso)
        Return True
    End Function
End Class

Cuando actualice el código con utilización de archivos adjuntos y/o la versión en C# avisaré como comentario del artículo.

Saludos.





¿Por que usar los tipos de datos de .NET Framework?

23 10 2007

Leyendo un poco en la Web, sobre todo sobre C#, es común encontrar a quien recomienda como buena práctica de programación, utilizar las palabras claves propias del lenguaje para los tipos de datos, en vez de utilizar las clases del Framework. No estoy de acuerdo con esto, no tanto con el enunciado, si no en clasificarlo como “buena práctica”.

Utilizar una u otra nomenclatura es indiferente para la ejecución del código, ya que los tipos de datos del lenguaje, tanto VB como C# son solo shorcuts a los tipos de nativos de .NET. Veamos que hay a favor y en contra de utilizar los tipos del framework:

  • (+) Es particularmente útil para quienes programamos en ambos lenguajes, por más asimilados que tengamos o no ambas sintaxis, siempre es más fácil utilizar la misma en ambos lenguajes, y aunque se utilize uno solo, será más facil para nosotros cuando tengamos que aprender el otro. También hay que considerar que no sabemos que background tendrá quien lea nuestro código en el futuro.
  • (+) El Visual Studio 2005+ resalta (solo C#) en un feo color ¿turquesa? los tipos de datos del framework, diferenciándolos del resto de las palabras claves.
  • (+) La sintaxis será soportada en futuros lenguajes .NET
  • (-) Hay que incluir un Imports/using del namespace System (aunque Visual Studio lo incluye en la mayoría, si no en todos los documentos.)
  • (-) Quienes venimos de otros lenguajes ya tenemos una sintaxis incorporada.
  • (-) Personas provenientes de otros lenguajes, particularmente C++, pueden ver afectada su virilidad.

Como vemos no hay razones de peso para utilizar uno u otro estándar, pocas para hacer una recomendación y creo que ninguna para calificar de “buena práctica” una u otra nomenclatura por sobre otra.

Para completar, algunos tips que no siempre se tienen presente y una tabla comparativa entre los dos lenguajes y el framework:

  • Short es más lento que integer, aunque pueda ocupar menos memoria.
  • Interger es el tipo numérico más eficiente en términos de rendimiento.
  • Los enteros sin signo (UInt16/32/64) son igualmente rápidos que los signados y con mayor capacidad, siempre que se sepa que solo se almacenaran enteros positivos, pero no son cls compliant.
  • Decimal” es el tipo con mayor rango pero las operaciones con este tipo son mucho más lentas que con el resto
    Double es el tipo real más rapido (en plataformas de 32 bits)
  • A partir del framework 3.5, existe una nueva estructura para albergar fechas y horas: DataTimeOffset, que maneja zonas UTC. Si bien sigue existiendo DateTime, DateTimeOffset será la recomendación de Microsoft.

Tabla comparativa

VB.NET C# .NET .NET Framework Almacenamiento Intervalo
Boolean bool Boolean true/false
Byte byte Byte 1 byte [0;255]
Short short Int16 2 bytes [-32768;32767]
Integer int Int32 4 bytes [-2147483648;2147483647]
Long long Int64 8 bytes [-10E19;10E19]*
Single float Single 4 bytes [-10E38;10E38]*
Double double Double 8 bytes [-10E308;10E308]*
Decimal decimal Decimal 12 bytes (parte entera) [-10E28;10E28]*
Date System.DateTime DateTime 8 bytes [1/1/0001;31/12/9999]
String string String
Char char Char 2 bytes
Object object Object 4bytes (32 bits) / 8 bytes (64 bits)
SByte sbyte Sbyte 1 byte [-128/127]
UShort ushort UInt16 2 bytes [0:65535]
UInteger uint UInt32 4 bytes [0;4.294.967.295]
ULong ulong UInt64 8 bytes [0;1,8E19]

* datos apróximados.

Saludos.





Fuslogvw ¿como encontrar una referencia perdida?

17 10 2007

Fusion es basicamente la tecnología utilizada en .NET para cargar los ensamblados en memoria. Es Fusion quien no encuentra nuestras referencias perdidas, y quien escribe un log que nos sirve para averiguar el porqué de un problema de este tipo.

Hay muchas circunstancias en las que podemos tener problemas para encontrar un ensamblado: versionado de assemblies, referencias desactualizadas, proyectos que se movieron, deployment, etc.

Cuando no encuentra un ensamblado, .NET (Fusion en particular) registra en un log las ubicaciones en donde Fusion lo buscó.

fuslogvw

La interfase es muy sencilla y una vez que encontramos el error que buscamos accedemos a un log similar a este:

*** Assembly Binder Log Entry  (17/10/2007 @ 12:15:31 a.m.) ***

The operation failed.
Bind result: hr = 0x80070002. The system cannot find the file specified.

Assembly manager loaded from:  C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\mscorwks.dll
Running under executable  C:\Program Files\Microsoft Visual Studio 8\Common7\IDE\devenv.exe
--- A detailed error log follows. 

=== Pre-bind state information ===
LOG: User = GS\Ale
LOG: DisplayName = Microsoft.Practices.EnterpriseLibrary.Configuration.Design.VisualStudioIntegration, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
 (Fully-specified)
LOG: Appbase = file:///C:/Program Files/Microsoft Visual Studio 8/Common7/IDE/
LOG: Initial PrivatePath = NULL
LOG: Dynamic Base = NULL
LOG: Cache Base = NULL
LOG: AppName = NULL
Calling assembly : System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089.
===
LOG: This bind starts in default load context.
LOG: Using application configuration file: C:\Program Files\Microsoft Visual Studio 8\Common7\IDE\devenv.exe.Config
LOG: Using machine configuration file from C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\config\machine.config.
LOG: Post-policy reference: Microsoft.Practices.EnterpriseLibrary.Configuration.Design.VisualStudioIntegration, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
LOG: GAC Lookup was unsuccessful.
LOG: Attempting download of new URL file:///C:/Program Files/Microsoft Visual Studio 8/Common7/IDE/Microsoft.Practices.EnterpriseLibrary.Configuration.Design.VisualStudioIntegration.DLL.
LOG: Attempting download of new URL file:///C:/Program Files/Microsoft Visual Studio 8/Common7/IDE/Microsoft.Practices.EnterpriseLibrary.Configuration.Design.VisualStudioIntegration/Microsoft.Practices.EnterpriseLibrary.Configuration.Design.VisualStudioIntegration.DLL.
LOG: Attempting download of new URL file:///C:/Program Files/Microsoft Visual Studio 8/Common7/IDE/PublicAssemblies/Microsoft.Practices.EnterpriseLibrary.Configuration.Design.VisualStudioIntegration.DLL.
LOG: Attempting download of new URL file:///C:/Program Files/Microsoft Visual Studio 8/Common7/IDE/PublicAssemblies/Microsoft.Practices.EnterpriseLibrary.Configuration.Design.VisualStudioIntegration/Microsoft.Practices.EnterpriseLibrary.Configuration.Design.VisualStudioIntegration.DLL.
LOG: Attempting download of new URL file:///C:/Program Files/Microsoft Visual Studio 8/Common7/IDE/PrivateAssemblies/Microsoft.Practices.EnterpriseLibrary.Configuration.Design.VisualStudioIntegration.DLL.
LOG: Attempting download of new URL file:///C:/Program Files/Microsoft Visual Studio 8/Common7/IDE/PrivateAssemblies/Microsoft.Practices.EnterpriseLibrary.Configuration.Design.VisualStudioIntegration/Microsoft.Practices.EnterpriseLibrary.Configuration.Design.VisualStudioIntegration.DLL.
LOG: Attempting download of new URL file:///C:/Program Files/Microsoft Visual Studio 8/Common7/IDE/Microsoft.Practices.EnterpriseLibrary.Configuration.Design.VisualStudioIntegration.EXE.
LOG: Attempting download of new URL file:///C:/Program Files/Microsoft Visual Studio 8/Common7/IDE/Microsoft.Practices.EnterpriseLibrary.Configuration.Design.VisualStudioIntegration/Microsoft.Practices.EnterpriseLibrary.Configuration.Design.VisualStudioIntegration.EXE.
LOG: Attempting download of new URL file:///C:/Program Files/Microsoft Visual Studio 8/Common7/IDE/PublicAssemblies/Microsoft.Practices.EnterpriseLibrary.Configuration.Design.VisualStudioIntegration.EXE.
LOG: Attempting download of new URL file:///C:/Program Files/Microsoft Visual Studio 8/Common7/IDE/PublicAssemblies/Microsoft.Practices.EnterpriseLibrary.Configuration.Design.VisualStudioIntegration/Microsoft.Practices.EnterpriseLibrary.Configuration.Design.VisualStudioIntegration.EXE.
LOG: Attempting download of new URL file:///C:/Program Files/Microsoft Visual Studio 8/Common7/IDE/PrivateAssemblies/Microsoft.Practices.EnterpriseLibrary.Configuration.Design.VisualStudioIntegration.EXE.
LOG: Attempting download of new URL file:///C:/Program Files/Microsoft Visual Studio 8/Common7/IDE/PrivateAssemblies/Microsoft.Practices.EnterpriseLibrary.Configuration.Design.VisualStudioIntegration/Microsoft.Practices.EnterpriseLibrary.Configuration.Design.VisualStudioIntegration.EXE.
LOG: All probing URLs attempted and failed.

En el, observamos lo datos del assembly que no logró bindear y las ubicaciones desde donde se intentó cargarlo.

Algunas cositas para tener en cuenta:

  • El log se guarda por defecto en el directorio de archivos temporales de Internet Explorer, si no se quieren perder datos se puede cambiar la ubicación desde la interfase.
  • También se puede configurar si se quieren ver solo los ensamblados con errores o todos los ensamblados bindeados. Esto puede ser muy útil si queremos controlar las referencias incluidas en nuestro proyecto, asegurandonos que no estamos levantando nada del lugar equivocado.
  • Previamente a la versión 2.0 del framework, fuslogvw.exe se configura mediante el registro de windows.

En esta página, un trabajo muy completo sobre Fusion, con mucha información de la GAC, Strong Name Key, etc. Incluyendo por supuesto fuslogvw.

Saludos





Operador ternario en c# y vb…y ??

11 10 2007

Algo creo que muy util de C# y que no existía en VB es el operador ternario, bueno en VB existe el IIf, pero hay una gran diferencia. Veamos:

En C#, la manera de utilizar el operador ternario es la siguiente

result = condición_lógica ? valor_si_verdadero : valor_si_falso;

Por ej.:

Int32 mayor = (a > b) ? a : b;

(los paréntesis no son necesarios, solo para legibilidad)

En este ejemplo clásico para devolver el mayor valor, si la condición lógica es verdadera ‘mayor’ valdrá a, caso contrario se le asignará b.

Ahora, que pasa si quiero utilizar el operador ‘?’ para lo siguiente:

String _usuario = (usuario != null) ? usuario : "invitado";

Bien, funciona, pero aquí puedo utilizar el operador ?? de C#, (o null coalescing operator) y obtener el mismo resultado con esta sentecia:

String _usuario = usuario ?? "invitado";

Este operador retorna el operando de la izquierda excepto cuando es nulo, donde retorna el de la derecha ‘invitado’ (ojo, no sirve para una cadena vacía, solo null)

Operador ternario en VB

A priori parecería que se puede utilizar el método IIf en VB como operador ternario, volviendo al primer ejemplo algo así como:

Dim mayor as Int32 = IIf(a > b, a, b)

¿funciona igual esto? sssNO. IIf es una función, por lo tanto retorna un tipo, ¿cual? Object. Y ahí se acabaron para mi todas la razones para utilizar IIf, ya que para no tener problemas y evitar casteos implicitos (una de las ‘licencias’ de VB que se aleja de la buena programación) tendríamos que convertir (CInt, CType…) el resultado antes de asignarlo a ‘mayor’.

Para colmo de males, como IIf es un método, todos los parámetros que se le pasan son evaluados previamente a la ejecución, por lo que se determinará el valor los dos posibles resultados sin importar cual sea el que retorne, más graficamente:

IIf(true, EstoSeEjecuta(), EstoYoNoQuisieraPeroSeEjecutaIgual())

Curioso: Si pasamos el código de C# a VB utilizando Reflector el operador ternario se refleja como IIf. Definitivamente Lutz sabe más que yo, pero creo que es una cuestión de legibilidad más que de exactitud.

En la MSDN David M. Kean propone hacer una función genérica, pero ¿que gracia tiene hacer una función, hacerla pública a todo el ámbito de nuetro código y sobre todo complicar la legibilidad del mismo que es la principal función del operador ternario, si no la única?

Todo esto hasta VB 8, en VB 9 tendremos la posibilidad de utilizar el operador ternario:

Dim mayor as Int32 = If(a > b ? a, b)

Tal vez se salga un poco de la clásica sintaxis de VB pero bienvenido sea.

En VB 9 (Agregado 11/2008): ¡Cuidado! Al ser el nuevo If de VB 9 un operador, ya no se calcula el valor de la expresión que no se retorna:

If(true, EstoSeEjecuta(), EstoNoSeEjecuta())

Pero de todas maneras el operador ¡Sigue retornando Object! y por lo tanto desaconsejo el uso del mismo para evitar boxing, casteos y demas temas.

Tambien hay que destacar que se agregó el null coalescing operator (o ??) de C#, con la siguiente sintaxis:

If(usuario, "Invitado")

Operador ternario y desempeño

En pruebas con Stopwatch, el desempeño es el mismo en ambos modos, no hay diferencia. A nivel IL el If genera más líneas, pero no redundan en el tiempo de ejecución.

Operador ternario y buenas prácticas de codificación

Acabo de leer por ahí a alguien jactandose de:

WriteLine("Login "+((condicion_booleana)?"":"in")+"válido.");

No estaré yo para competir en TopCoders, pero esto es lo que se llama algo muy muy poco recomendable, no pasa por el operador ternario el problema, si no por el hecho de querer ahorrar código haciendo malabares que después otro tendrá que entender. Esto es pereza no eficiencia.

Por último. En más de un documento donde se habla de buenas prácticas de codificación, recomiendan no usar el operador ternario, ¿por que?, bueno entiendo que lo que persiguen quienes hacen esta recomendación es código legible, pero lo que está mal es el abuso del operador, no su uso normal. Por ejemplo no es legible la sentecia:

cena = estoyPaNioquis ? HacerNioquis(harina, agua, huevos, pure):

PedirPizza(4866-2804,'Napo');

Cuando una línea excede la pantalla, ¿partimos la sentencia con el operador ternario en varias líneas? No. utilizamos If. Nuevamente la ventaja de este operador es unicamente la legibilidad del código, si no, no tiene gracia.

False: Me voy a comer pizza.





Sobre @@error, @@rowcount y porqué tener cuidado al utilizarlas juntas

6 10 2007

La manera de manejar los errores en sql server al estilo de la versión 2000 (o < ) es con @@error, que guarda el número de error que se produjo o 0 si no hubo ninguno; y en todas las versiones se utiliza @@rowcount para averiguar la cantidad de filas afectadas por una sentencia. Hay que tener en cuenta algunos detalles para evitar problemas con estas dos variables.

Ambas variables tienen el valor correspondiente luego de cada sentencia, pero este valor cambia en ambas al ejecutar otra, cualquiera que esta sea. Por ejemplo:

INSERT NOMBRES (NOMBRE, APELLIDO) VALUES ('MORCILLO','LOPEZ')
IF @@ERROR <> 0 THEN RETURN @@ERROR

Da como resultado 0 para @@error por mas que la inserción falle, debido a que luego de la sentencia IF se limpia el contenido de @@error. Una manera correcta de manejar esto sería:

DECLARE @@CODIGO_ERROR INT
INSERT NOMBRES (NOMBRE, APELLIDO) VALUES ('MORCILLO','LOPEZ')
SET @@CODIGO_ERROR = @@ERROR
IF @@CODIGO_ERROR <> 0 THEN RETURN @@CODIGO_ERROR

Garantizando así que el error retornado es correcto.

Sobre @@rowcount hay que tener las mismas precauciones, o sea, guardar en una variable el resultado siempre que lo necesitemos para algo.

Ahora ¿como hacer para utilizar ambas variables sin perder ninguna? La solución es asignar ambas en una sola sentencia:

DECLARE @@CODIGO_ERROR INT
DECLARE @@RCOUNT INT
INSERT NOMBRES (NOMBRE, APELLIDO) VALUES ('MORCILLO','LOPEZ')
SELECT @@CODIGO_ERROR = @@ERROR, @@RCOUNT = @@ROWCOUNT
IF @@CODIGO_ERROR <> 0 THEN RETURN @@CODIGO_ERROR
IF @@RCOUNT...

La sentencia Select es una sola y asigna las dos variables.

Sobre error handling en sql server, hay un artículo (en inglés) de Erland Sommarskog que recomiendo para tener una idea completa del tema. (así como todos los artículo de Erland, que son algo así como “la verdad” de cada tema de sql sobre el que publica)

Por último, me gusta ser cuidadoso el escribir sql, tabulaciones, alias legibles, etc. Por este mismo motivo me gusta poner las sentencias de error handling en una sola línea: (el ejemplo con rollback de la transacción)

SET @@CODIGO_ERROR = @@ERROR /**/ IF @@CODIGO_ERROR <> 0 THEN /**/
BEGIN /**/ ROLLBACK TRAN /**/ RETURN @@CODIGO_ERROR /**/ END

Cuestión de gustos…