domingo, 15 de agosto de 2010

[N-Tier] – Desarrollo en capas - Ejemplo Facturación – Parte 3

Introducción


Hemos entrado en un momento clave en la evolución del presente del articulo, aquí es donde concretamente se verán las 3 capas interactuando entre si.

El mismo fue evolucionado de artículos previos:

[N-Tier] – Desarrollo en capas - Ejemplo Facturación – Parte 1

[N-Tier] – Desarrollo en capas - Ejemplo Facturación – Parte 2

En la ultima oportunidad se había reestructurado la aplicación para que soportara 2 capas, la presentación accedía directo a la de datos para trabajar con las entidades.

Además se había agregado un proyecto que representa las entidades del negocio, el cual cruzaba todas las capas y era usado como medio de trasporte de datos entre las mismas. Este estaba implementado por medios de dataset tipado para representar las entidades.

Estructura del proyecto


La nueva arquitectura agrego un proyecto adicional del tipo “Class Library”, el cual se ubicara en medio de la capa de presentación y la de datos, desacoplándolas.

Esta nueva capa representara la fachada de entrada al dominio de la aplicación, mas adelante cuando se implementen servicio para distribuir la aplicación cumplirá un papel fundamental para aislar el dominio.

También se reestructuro el proyecto de entidades, ahora ya no se usan dataset tipados para representar las entidades, estas fueron reemplazas por clases custom, es por eso que se verán  nombre como ser:  “CustomerEntity”, “InvoiceEntity”, etc.

El cambio en las entidades afecto la capa de datos, ya no se usa el DataAdapter para cargar los datatable que representaban a la entidad, fue necesario reemplazarlos por DataReader, estos son óptimos para la lectura secuencial de los registros devueltos por la query, y el armado de las instancias de la entidad.

La imagen representa los distintos proyectos y como se referencian entre si:

 image

En esta nueva distribución de capas será imprescindible que la presentación se comunique siempre con la fachada de negocio, la cual abstraerá las operaciones transaccionales, y creara un único punto de entrada al sistema, si bien no se aprecia la importancia de lo dicho con este ejemplo, si a futuro fuera necesario cambiar la presentación, quizás por una web o con WPF, no se perdería todo el trabajo realizado, ya que las reglas de negocio y persistencia quedan intacticas.

Algo que seguro traer molestia al desarrollar aplicando esta técnica es que la mayoría de las operaciones serán un pasamano por la capa de negocio, esta solo tomara lo que la presentación retorne y lo devolverá ala presentación, sin efectuar ninguna operación en medio, para la mayoría de las operaciones de consulta será así, pero en otras circunstancias se vera la importancia de esta capa, sobre todo al persistir entidades complejas.

El uso de entidades con clases en lugar de dataset tipados, también aporta una mejora importante, las clases permiten extender funcionalidad y relacionar entidades fácilmente, como ser el caso del calculo de Total en la entidad de facturación.

A continuación se analizarían las operaciones que han sufrido cambios durante la transformación a las 3 capas.

Grabar/Actualizar un Cliente


Durante la operación de confirmación de la factura se notara el cambio en la técnica utilizada para persistir la información del cliente, anteriormente desde la presentación se decidía si se actualizaba o insertaba la entidad, ahora es la capa de negocio quien decide que operación debe realizarse.

[Presentación]

if (cliente == null)
    cliente = new CustomerEntity();

cliente.FirstName = txtNombre.Text;
cliente.LastName = txtApellido.Text;
cliente.Company = txtCompañia.Text;
cliente.Address = txtDireccion.Text;
cliente.Email = txtEmail.Text;

cliente = CustomerBO.Save(cliente);

[Business Layer]

public static CustomerEntity Save(CustomerEntity customer)
{

    if (CustomerDAL.Exists(customer.CustomerId))
        return CustomerDAL.Update(customer);
    else
        return CustomerDAL.Create(customer);

}

[Data Access]

public static class CustomerDAL
{

    public static bool Exists(int id)
    {
        int nrorecord = 0;

        using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
        {
            conn.Open();

            string sql = @"SELECT Count(*)
                            FROM Customer 
                            WHERE CustomerId = @customerId";

            SqlCommand cmd = new SqlCommand(sql, conn);
            cmd.Parameters.AddWithValue("customerId", id);

            nrorecord = Convert.ToInt32(cmd.ExecuteScalar());
        }

        return nrorecord > 0;

    }

    public static CustomerEntity Create(CustomerEntity customer)
    {
        using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
        {
            string sql = @"INSERT INTO Customer (FirstName, LastName, Company, Address, Email) 
                                VALUES (@firstName, @lastName, @company, @address, @email)
                           SELECT SCOPE_IDENTITY()";

            SqlCommand cmd = new SqlCommand(sql, conn);

            cmd.Parameters.AddWithValue("@firstName", customer.FirstName);
            cmd.Parameters.AddWithValue("@lastName", customer.LastName);
            cmd.Parameters.AddWithValue("@company", customer.Address);
            cmd.Parameters.AddWithValue("@address", customer.Company);
            cmd.Parameters.AddWithValue("@email", customer.Email);

            customer.CustomerId = Convert.ToInt32(cmd.ExecuteScalar());
        }

        return customer;
    }

    public static CustomerEntity Update(CustomerEntity customer)
    {
        using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
        {
            conn.Open();

            string sql = @"UPDATE Customer SET  
                                        FirstName = @firstName, 
                                        LastName = @lastName, Company = @company, 
                                        Address = @address,
                                        Email = @email
                                WHERE CustomerId = @customerid";

            SqlCommand cmd = new SqlCommand(sql, conn);

            cmd.Parameters.AddWithValue("@firstName", customer.FirstName);
            cmd.Parameters.AddWithValue("@lastName", customer.LastName);
            cmd.Parameters.AddWithValue("@company", customer.Address);
            cmd.Parameters.AddWithValue("@address", customer.Company);
            cmd.Parameters.AddWithValue("@email", customer.Email);
            cmd.Parameters.AddWithValue("@customerid", customer.CustomerId);


            cmd.ExecuteNonQuery();

        }

        return customer;
    }


}

La capa de negocio valida si la entidad existe o no, y procede a ejecutar la operación correcta para cada caso, usando como identificador el id de la entidad.

Proceso de Facturación


Con respecto a la implementación del articulo previo el proceso de facturación sufrió un cambio importante, ya no se envía dos entidades separadas para procesar, las cuales representaban al encabezado de la factura y sus líneas, sino que una única entidad posee una colección o lista genérica vinculada que permite cargar los datos de la asociación.

[Presentación]

#region Creo el Encabezado\Linea de la Factura

InvoiceEntity invoice = new InvoiceEntity();

invoice.CustomerId = cliente.CustomerId;
invoice.InvoiceDate = DateTime.Now.Date;
invoice.BillingAddress = txtDireccion.Text;


foreach (DataGridViewRow row in dgvLineaCompra.Rows)
{
    InvoiceLinesEntity invoiceLine = new InvoiceLinesEntity();

    invoiceLine.TrackId = Convert.ToInt32(row.Cells["Track"].Value);
    invoiceLine.UnitPrice = Convert.ToDecimal(row.Cells["PrecioUnitario"].Value);
    invoiceLine.Quantity = Convert.ToInt32(row.Cells["Cantidad"].Value);

    invoice.Lineas.Add(invoiceLine);
}

InvoiceBO.RegistrarFacturacion(invoice);

#endregion

[Business Layer]

public static class InvoiceBO
{
    public static void RegistrarFacturacion(InvoiceEntity invoice)
    {
        //
        // inicializo la transacciones
        //
        using (TransactionScope scope = new TransactionScope())
        {
            //
            // Creo la factura y sus lineas
            //
            InvoiceDAL.Create(invoice);

            //
            // Actualizo el total
            //
            InvoiceDAL.UpdateTotal(invoice.InvoiceId, invoice.Total);
            
            scope.Complete();
        }

    }
}

[Data Access]

public static class InvoiceDAL
{

    public static void Create(InvoiceEntity invoice)
    {
        using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
        {
            conn.Open();
            //
            // Creacion de la Factura
            //
            string sqlFactura = @"INSERT INTO Invoice (CustomerId, InvoiceDate, BillingAddress, Total) VALUES (@customerId, @date, @address, @total)
                       SELECT SCOPE_IDENTITY()";

            using (SqlCommand cmd = new SqlCommand(sqlFactura, conn))
            {

                cmd.Parameters.AddWithValue("@customerId", invoice.CustomerId);
                cmd.Parameters.AddWithValue("@date", invoice.InvoiceDate);
                cmd.Parameters.AddWithValue("@address", invoice.BillingAddress);
                cmd.Parameters.AddWithValue("@total", 0);

                invoice.InvoiceId = Convert.ToInt32(cmd.ExecuteScalar());
            }


            string sqlLineaFactura = @"INSERT INTO InvoiceLine (InvoiceId, TrackId, UnitPrice, Quantity) 
                                        VALUES (@invoiceid, @trackid, @unitprice, @quantity)
                                        SELECT SCOPE_IDENTITY()";

            using (SqlCommand cmd = new SqlCommand(sqlLineaFactura, conn))
            {

                foreach (InvoiceLinesEntity invoiceLine in invoice.Lineas)
                {
                    //
                    // como se reutiliza el mismo objeto SqlCommand es necesario limpiar los parametros
                    // de la operacion previa, sino estos se iran agregando en la coleccion, generando un fallo
                    //
                    cmd.Parameters.Clear();

                    cmd.Parameters.AddWithValue("@invoiceid", invoice.InvoiceId);
                    cmd.Parameters.AddWithValue("@trackid", invoiceLine.TrackId);
                    cmd.Parameters.AddWithValue("@unitprice", invoiceLine.UnitPrice);
                    cmd.Parameters.AddWithValue("@quantity", invoiceLine.Quantity);

                    //
                    // Si bien obtenermos el id de linea de factura, este no es usado
                    // en la aplicacion
                    //
                    invoiceLine.InvoiceLineId = Convert.ToInt32(cmd.ExecuteScalar());
                }

            }

        }

        

    }

    /// <summary>
    /// Actualizacion del Total de la Factura
    /// </summary>
    public static void UpdateTotal(int idInvoice, decimal total)
    {
        using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
        {
            conn.Open();

            string sqlUpdateTotal = @"UPDATE Invoice SET Total = @total WHERE InvoiceId = @InvoiceId";

            using (SqlCommand cmd = new SqlCommand(sqlUpdateTotal, conn))
            {
                cmd.Parameters.AddWithValue("@total", total);
                cmd.Parameters.AddWithValue("@InvoiceId", idInvoice);

                cmd.ExecuteNonQuery();
            }
        }
    }

}

 

Un punto importante a remarcar es que ahora la capa de negocio orquesta todas las operaciones y por ende es esta la que lleva la transacción de las entidades, en el artículo anterior se había comentado este punto, justamente porque la presentación no debía ser responsable de asegurar la operación. Es mas a la presentación se le ha quitado la referencia a la librería System.Transactions.

Durante la transformación se separo una de las funcionalidades correspondiente a al actualización del total de la factura, en el ejemplo previo se hacia la sumatoria mientras se creaba cada línea, y al final se actualizaba la entidad “invoice” con el valore resultante. Ahora esta se realiza en una operación separada y coordinada por la capa de negocio, se toma el id de factura devuelto por la operación anterior, y se sumando los valores de las línea internamente en la propia entidad que representa la factura de cliente, ya que ahora esta posee la colección relacionada, una operación lambda en el método de extensión de suma fue mas que suficiente.

public class InvoiceEntity
{
    public InvoiceEntity()
    {
        this.Lineas = new List<InvoiceLinesEntity>();
    }

    public int InvoiceId { get; set; }
    public int CustomerId { get; set; }
    public DateTime InvoiceDate { get; set; }
    public string BillingAddress { get; set; }

    public List<InvoiceLinesEntity> Lineas { get; set; }

    public decimal Total
    {
        get { return this.Lineas.Sum(x => x.UnitPrice * x.Quantity); }
    }
}

Conclusión


Si bien aun quedan muchos puntos por explorar, este simple ejemplo puede servir de guía para comenzar con futuros desarrollos.

Es cierto que algunos aspectos que podrían haberse implementado, como ser:

- La entidad InvoiceEntity aun conserva la propiedad CustomerId, cuando debería reemplazarse por una propiedad del tipo CustomerEntity, lo cual no se realizo para no aumentar la complejidad.

- No se implementaron controles fuerte de errores y como comunicarlos hacia la presentación para ser tratados, este punto se vera en un articulo futuro.

- No se agregaron reglas de negocio restrictivas, como podría ser, por ejemplo, si el cliente supera tiene un monto determinado de facturas impagas no se permita la facturación en curso.

En posteriores artículos tratare estos tema con mas detalle, y otros como ser:

-la utilización de reportadores como Crystal Reports o Reporting Service en aplicación con capas,

-la creación de repositorios de acceso a datos que soporten distintas base de datos,

-el cambio de la presentación a un proyecto web para marcar la reutilización, así como también al desconexión de la capas de presentación y negocio mediante servicios para poder hacer uso de un ambiente distribuido.

 

Ejemplo de código


El proyecto fue desarrollado con Visual Studio 2008

Se debe tener presente Sql Server 2008 Express para poder acceder a la db integrada al proyecto.

 

[C#] 
[VB.NET] 

137 comentarios:

  1. Muy bueno, Leandro, gracias una vez mas por compartirlo.

    ResponderEliminar
  2. Hola, muy bueno el aporte Leandro, la verdad es que estoy aprendiendo mucho con este proyecto de desarrollo en capas.

    ResponderEliminar
  3. Buenas noches, baje los archivos, pero no me funcionan si alguien sabe, que debo hacer se los agradezco

    ResponderEliminar
  4. hola Lixander

    Has validado los requisitos que menciono al final del articulos

    O sea estas usando VS2008 y Sql Express 2008 ? tienes el servicio de sql server iniciado

    Por ahi si mencionas el problema, o mensaje que obtienes, pueda dar alguna pista mas precisa del problema puntual

    saludos

    ResponderEliminar
  5. Buenas tardes,

    Gracias por tu respuesta.
    Estoy trabajando con visual studio 2008 y sql server 2008.
    Este es el error.
    The database 'D:\TRANSACCIONES\[VB.NET]CHINOOK3CAPAS\CHINOOK\BIN\DEBUG\CHINOOK.MDF' cannot be opened because it is version 655. This server supports version 612 and earlier. A downgrade path is not supported. Could not open new database 'D:\TRANSACCIONES\[VB.NET]CHINOOK3CAPAS\CHINOOK\BIN\DEBUG\CHINOOK.MDF'. CREATE DATABASE is aborted. An attempt to attach an auto-named database

    ResponderEliminar
  6. hola Lixander

    Por lo que puedo ver del mensaje de error, es que la version de sql Server Express que usas no es la 2008. Ese mensaje esta indicando que la version del .mdf fue creado con una version mas nueva que el motor de base de datos que tienes instalado.

    Prueba de descargar he instalar la version que encuentras en este link

    Sql Server Express 2008

    Esta es la que yo use

    saludos

    ResponderEliminar
  7. Leandro, que pena, pero esto afectaria en algo las claves y las bases de datos que tengo registradas en sql server 2008.
    En otras palabras me afecta el desarrollo que tengoen sql server 2008

    ResponderEliminar
  8. Hola
    Leandro ¿tiene que ser necesariamente la version express del 2008? o puede ser simplemente la 2008.Además el link que pusiste ahí lleva a la descarga de un ejecutable que parece un virus, luego que elijes a cual de los windows quieres instalar al de 32 o 64.
    Saludos.

    ResponderEliminar
  9. hola Augusto

    Si puedes usa la version 2008 del Sql Server sin problemas, recomende la version express porque es lo mas practico para tener en una PC de escritorio, las versiones mayores de sql server suelen estar en servidores de base de datos.

    Por lo del virus y el .exe, la verdad no sabria que decirte, accedi al archivo, lo descargue en mi pc y no tuve problema, es mas no hay ningun .exe dentro del zip.

    Tampoco tiene ningun instador el ejemplo es un simple zip, no eliges version alguna, estas seguro que es el link del articulo el que estas usando.

    saludos

    ResponderEliminar
  10. hola, te queria consultar como podria hacer si quisiera en el metodo Save de la capa de negocio, que si ya existe el cliente me envie un mensaje notificandomelo (que no haga el Update) y si no existe que lo cree.
    Mi duda es como pasar este mensaje de la capa de nogocio para que lo tome la capa de presentacion y lo muestre desde ya muchas gracias.

    ResponderEliminar
  11. hola Bruno

    disculpa la demora en contestar

    Lo que podrias hacer es lanzar una Exception desde la capa de negocio, que por supuesto atraparas en la capa de datos y mostraras el mensaje.

    Lo ideal seria que no sea una Exception cualquiera sino una creada por ti, de forma especial
    justamente como explico aqui:

    Control de Errores para proyecto en capas

    veras en el link que se lanza una BusinessException, indicando que es un error procedente de una validacion del negocio, no es un error cualquiera, por lo tanto lo atrapas y lo muestras de forma diferentes

    saludos

    ResponderEliminar
  12. hola Leandro gracias por tu respuesta, la duda que tengo es que esta bien si lanzo la excepcion en la BLL y la atrapo en la UI?, no entiendo por que la deberia atrapar en la DAL.
    Yo hice lo siguiente segun el tu articulo del link:
    defini una clase BussinnesException en un proyecto Comun, que es donde tb estan las Entidades por jemplo;

    namespace Comun.Excepciones
    {
    public class BussinessExcepcion: ApplicationException
    {
    public BussinessExcepcion()
    : base("Ya existe la Cuenta") {}

    luego lanzo la excepcion en la BLL:

    public static Cuentas Crear(Cuentas cuentas)
    {
    if (!CuentasDAL.Existe(cuentas.Nro_cuenta))
    return CuentasDAL.Crear(cuentas);
    else
    throw new Comun.Excepciones.BussinessExcepcion();
    }

    y luego la capturo en la UI:

    try {.........
    cuentas = CuentasBLL.Crear(cuentas);}
    catch (Comun.Excepciones.BussinessExcepcion ex)
    {MessageBox.Show(ex.Message);}

    es asi como se propaga una excepcion?

    Por otra parte como puedo hacer para que la clase BussinessExcepcion que hice me muestre distintos tipos de mensajes como en el ejemplo que pusiste:
    throw new BusinessException("Usuario no existe");
    y
    throw new BusinessException("Usuario bloqueado");

    muchas gracias nuevamente.

    ResponderEliminar
  13. hola

    la duda que tengo es que esta bien si lanzo la excepcion en la BLL y la atrapo en la UI?

    disculpa me equivoque al escribirlo, es correcto en UI atrapas la Exception

    es asi como se propaga una excepcion?

    perfecto has entendi de maravilla es justo donde apuntaba

    como puedo hacer para que la clase BussinessExcepcion que hice me muestre distintos tipos de mensajes

    podrias usar:

    public class BussinessExcepcion: ApplicationException
    {
    public BussinessExcepcion() : base("")
    {
    }

    public BussinessExcepcion(string message) : base(message)
    {
    }
    }

    como veras se define un constructor con el mensaje, y otro sin ninguno.

    saludos

    ResponderEliminar
  14. Excelente articulo, es uno de los pocos ejemplos claros, entendibles y que ademas funcionan.
    Te comento que como desarrollador C# siempre chequeo tu pagina en busca de tips.
    Saludos

    ResponderEliminar
  15. El que tiene otra version de sql2008(no express) lo que puede hacer es:
    En el "sqlServer Management Studio" adjuntar la DB
    , luego en visualStudio, en el serverExplorer conectarse al servidor, presionar boton derecho sobre la DB, elegir propiedades y copiar el Connection String

    , luego ir a app.config y remplazar el connectionString por el correcto
    , luego f5

    Saludos
    Martin

    ResponderEliminar
  16. cómo sería el metodo para eliminar una item de compra en la grilla?

    Saludos
    Martin

    ResponderEliminar
  17. Primero que nada... felicidades Leonardo, excelente blog!...

    La verdad es que sigo tu blog y el de JuanK.. ;)

    @Martin

    Lo ideal es borrar de la lista el producto, y luego refrescar el DataSoruce del DataGridView..

    Una forma seria utilizar la propiedad del DataGridView..

    Lista.RemoveAtDGV.CurrentCell.RowIndex);

    Luego le pones null al datadource y le asignas la lista.... por desgracia el metodo refresh del DGV no hace ese trabajo :S.

    PD: Leonardo, aun sigo esperando como enlazar el errorprovider desde BussinessLayer con la capa de presentacion, para manejarlos de manera centralizada :P


    SuerteX :)

    ResponderEliminar
  18. hola Alan

    Pero el ErrorProvider es un control que trabaja a nivel de presentacion y la capa de negocio esta desconectada de este

    Quizas se pueda enlazar en el evento Validating de algun control, invocando a la funcionalidad de validacion de la capa de negocio, realizando alguna verificacion que devuelva si esta pasa (o no pasa) con la info que se le provea

    entonces si la capa de negocio dice que no paso, activas el ErrorProvider en la presentacion, pero no se me ocurre algo centralizado


    saludos

    ResponderEliminar
  19. Gracias por la publicacion fue de mucha ayuda, pero tengo una gran duda, cual es la mejor forma de manejar las excepciones, por ejemplo un error de conexion a la BD, esta poder pasarla a la capa de presentancion (web form) y almacenar la exepcion original en un log.

    ResponderEliminar
  20. hola Freddy

    poidrias implementar algo como lo comentado en una consulta del foro de msdn

    http://social.msdn.microsoft.com/Forums/es/vcses/thread/31410a75-d0a3-4264-b910-324fc8a576d6

    tambien podrias implementar un control global de errores

    [Winforms] Control global de Errores – Implementar log

    saludos

    ResponderEliminar
  21. Muchas Gracias Leandro... ya entendi el concepto.

    ResponderEliminar
  22. En este caso, utilizarias una capa para el manejo de errores? Como los manejarias?

    ResponderEliminar
  23. hola Ele

    lo haria de la misma forma en que lo explique en estos casos

    http://social.msdn.microsoft.com/Forums/es/vcses/thread/31410a75-d0a3-4264-b910-324fc8a576d6

    http://social.msdn.microsoft.com/Forums/es/vcses/thread/a5d62a43-f4e8-4b9f-b871-f4c02081368d

    saludos

    ResponderEliminar
  24. Hola. El Using antes de la conexión emula de cierta forma el:

    conexion.Open()
    ////
    conexion.Close()

    ?

    ResponderEliminar
  25. hola educationchildren

    el using asegura una seccion en donde la conexion estara activa y disponible

    el Open() debes hacerlo, salvo que uses un DataAdapter que lo hace automatico, peor con el ExecuteNonQuery() y con un DataReder debes hacer el open

    lo que si no es necesario es el Close

    saludos

    ResponderEliminar
  26. Leandro una consulta es posible utilizar NuevaLinea() en el evento selectedIndexChanged del dgvLineaCompra pero despues de colocar la cantidad. Asi no utilizo el boton +?

    Gracias

    ResponderEliminar
  27. hola Richard

    pero de esa forma estarias agregando un nuevo registro por cada cambio de seleccion de la fila del grid, no lo vio muy controlado a esa accion

    quizas podrias si asignar al grid una tecla que agregue registros, por ahi el Alt+ A, o alguna otra que lance la accion

    podrias usar el ProcessCmdKey para detectar esta accion sobre el datagridview

    http://social.msdn.microsoft.com/Forums/es/vcses/thread/4fa3d568-5e37-4ec9-bea6-e50da352d65a

    saludos

    ResponderEliminar
  28. Gracias, lo voy a probar. Feliz Año Nuevo Leandro

    ResponderEliminar
  29. como pasar datos de un texbox a un lixbox

    ResponderEliminar
  30. hola josias

    no encontre mucha relacion entre esta pregunta y el tema del articulo

    pero bueno, agregar un item a un listbox seria tan simple como usar

    private void btnagregar_Click(..){

    ListBox1.Items.Add(TextBox1.Text);

    }


    saludos

    ResponderEliminar
  31. Leandro;

    Enhorabuena por la calidad de tus artículos, y gracias por la labor que haces para la comunidad .NET.

    Unas preguntas:

    En la aplicaición "[N-Tier] – Desarrollo en capas - Ejemplo Facturación – Parte 3", no veo cómo cierras las conexiones y los datareader, ni tampoco cómo capturas los errores (Try Catch).

    He leído """el Open() debes hacerlo, salvo que uses un DataAdapter que lo hace automatico, peor con el ExecuteNonQuery() y con un DataReder debes hacer el open

    lo que si no es necesario es el Close""". Pero no es necesario hacer el close del datareader y de la conexión.

    ¿Alguna explicación?. Muchas gracias. Un saludo.

    ResponderEliminar
  32. hola Álvaro

    las conexiones no las cierro porque estas las defino en un bloque "using"
    este me asegura que el objeto se destruye

    no le implemente un control de errores al desarrollo para no complicarlo
    pero podria aplicarse algo como ser

    [Winforms] Control global de Errores – Implementar Log


    exacto con el dataadapter el open no es necesario, con el datareader si
    pero el cierre como comente no lo haces porque al salir del bloque "using" se efectua esta operacion
    igualmente recuerda que ado.net mantiene un pool de conexion, por lo tanto el cierre en si no es tan asi, sino que mentiene la conexion vigente por si se estable ce una nueva y reutilizar las instancias

    saludos

    ResponderEliminar
  33. Gracias Leandro;

    Unas preguntas:

    1-En la capa de acceso a datos, ¿hay que crear sólo clases que representen tablas de la BBDD?, es decir, si en la BBDD hay 3 tablas, ¿en la capa de acceso a datos habrá 3 capas?.

    2- En la capa de negocio, ¿hay que crear las clases que enlazan con las clases de la capa de datos, como has hecho tú con Customer, Invoice y Track, que existen tanto en la capa de negocio como en la de acceso a datos, más clases que no tienen correspondencia con tablas en la BBDD, pero tienen cierta lógica de negocio, como pudieran ser clases para elaborar informes, y que van a llamarse desde los formularios de la capa de presentación?.

    Gracias, un saludo.

    ResponderEliminar
  34. hola Álvaro

    1 -
    imagino habras querido decir que debe haber 3 clases, en lugar de capas

    en principio no es una regla que sea asi, si es lo mas comun, pero podrias hacer que una tabla se represente por mas de una clase, porque en .nmet pdorias representar herencia y demas asociaciones que mepeen a mas de una tabla

    2-
    la respuesta es similar a ala enterior, no es una regla que sea asi, podrias crear funcionalidad con clases con nombre difernte en la capa de negocio que haga uso de la capa de datos de forma distinta

    por ejemplo podria crear clases de negocio relacionado a un CRM que haga uso de la info del customer, en este caso la clase ya no tendran uno a uno en el nombre

    recuerda que aqui implemente un ejemplo simple que puede aplciar los conceptos basicos, mientras se respeten las responsabilidades de cada clase alcanza para que sea separacion en capas, despues los nombre pueden variar, o sea mientras desde la presentacion no te conectes directo a los datos, eso si seria algo incorrecto

    saludos

    ResponderEliminar
  35. Hola, gracias por los aportes. Tengo 2 dudas a ver si podes ayudar. Estoy con una app para windows, si uso base de datos sqlserver es necesario instalar el servidor en la pc cliente?. y la otra es que neceisot saber saber como usar las consultas q tengo guardadas dentro de la base de datos access.
    Saludos y gracias

    ResponderEliminar
  36. hola Juan

    el servicio de sql server siempre es necesario instalarlo, ya sea en la pc del cliente o en una pc que actuara como servidor

    no entendi lo de la consulta en access, te refieres a una vista ?

    saludos

    ResponderEliminar
  37. Hola, access te permite guardar dentro de la base de datos (tablas, vistas, macros, formularios y consultas). Mi idea seria definir las consultas ahí y poder usarlas "tipo" procedimiento almacenado. Gracias

    ResponderEliminar
  38. hola Juan

    vistas y tablas eso no hablia problemas es lo que normalmente se usa en una db
    por consultas imagino seria lo mismo que vistas

    lo que no estoy seguro es que tan bueno es access con los stored procedure

    aqui se trato el tema
    http://social.msdn.microsoft.com/Forums/es/vbes/thread/d3167c4d-8ca2-41d4-94a5-6405ea35c104

    usarias lo recomendado por Enrique para ejecutarlos

    pero me pregunto no te animas a usar sql server express, no es tan complciado y es muchas veces mas robusto que access

    saludos

    ResponderEliminar
  39. Hola Leandro,
    Antetodo muchas gracias por el aporte. Lo he realizado con exito.

    Quisiera saber si me puedes ayudar en aumentarle alguna funcionalidad como es la modificacion de una factura ya existente.

    1. Como hacer para que una SELECT me presente datos de mas de una tabla, esto lo estoy haciendo con el entity de la factura al cual le he añadido campos para que me devuelva un registro de la factura y ademas el apellido del cliente. Al InvoiceEntity.cs le he añadido la propiedad apellido, se que quizas no se debe asi, lo estoy haciendo solo para probar y si me da el resultado que deseo. Esto, con el fin de usarlo en una ventana analoga a la ventana de buscar cliente.

    2. Como se haria la programacion para modificar una factura(cabecera y detalle) considerando que cuando extraemos una factura de la base de datos, logicamente en el cliente le podemos añadir, modificar, borrar items en el detalle.

    Muchas gracias de antemano por las pistas y ayuda en general a este tema.
    Saludos,
    Eusebio.

    ResponderEliminar
  40. hola eestradaa

    pero se supone que la factura tiene uan relacion con la tabla clientes, por lo que seria una asociacion con esta entidad, no necesitas de una propiedad apellido porque se supone que debes poner una propiedad que sea

    public ClienteEntity Cliente {get; set;}

    para asi cargar los datos completos del cliente asociado a la factura

    La modificacion no seria muy distinta al alta, solo cambiando los INSERT pot UPDATE, por el unico caso del detalle, porque se supone que puede agregar eliminar o actualizar una linea lo cual implica un MERGE de toda la informacion con resepcto a la que se tiene aun en la tabla de la db, esto puede ser algo complejo, por eso se suele quitar todas las linea para la factura he insertar nuevamente, quizas no sea lo mejor pero hacer una comparacion completa podria ser algo complejo de programar

    saludos

    ResponderEliminar
  41. En tu comentario: "mas adelante cuando se implementen servicio para distribuir la aplicación cumplirá un papel
    fundamental para aislar el dominio." podrias hacer o tener un ejemplo para la distribucion de la aplicacion segun la metodologia que haces aqui?

    Aunque los Datareader consumen menos recursos que los DAdapter y Dset en un ambiente totalmente desconectado no nos traeria problemas?

    ResponderEliminar
  42. hola greg_dorian

    estoy pensando en armar un ejemplo usando servicios para mostrar como seria la comunicacion
    pero queria basarlo en algo mas simple, aun no lo tengo definido

    no deberia tener problemas, he usado reader para cargar entidades de clase mucho tiempo y no se han presentado inconvenientes

    nunca te has parado a pensar como se carga un dataset? o que crees que usa internamente el DataAdapter cuando haces un Fill(), si usa un datareader para cargar al estructura del datatable

    saludos

    ResponderEliminar
  43. gracias por todo felicitarle por la enseñanza

    ResponderEliminar
  44. Hola Leandro, tus post son muy instructivos, quisiera que libro me puedes recomendar para aprender todo sobre Desarrollo en Capas o algunas pagina en especial.

    Saludos

    Fritz

    ResponderEliminar
  45. Buenas Leandro, como todos los demás te felicito por lo buenos de tus post y lo profesional de tu manera de programar. Te quiero hacer una pregunta breve y otra mas extensa pero que tienen relación entre si.
    Primero: ¿porque no se usa en este post la comunicación entre formularios por medio de una interface como lo haces en tu post http://ltuttini.blogspot.com.ar/2010/01/c-datagridview-parte-3-pasaje-de.html?, el mismo me parece muy bueno y me brindo mucha ayuda.
    Por otro lado, y también relacionado a lo mismo, tengo 3 formularios, Principal, Clientes y Ciudades. Ciudades es llamado tanto desde Principal como desde Clientes, con la diferencia que al abrir Ciudades desde Clientes necesito que me devuelva el nombre de la ciudad seleccionada en una grilla. Pero cuando se llama desde Principal, no tiene que devolver nada.
    No se si me aclaro?, use la interface de tu otro post, pero como la llamada al evento implementado de la interface de comunicación lo hago desde el closing de ciudades, se me ejecuta siempre (sea que a Ciudades lo abrió Principal o lo abrió Clientes).
    No quiero liarme con la explicación, si hace falta paso el código para que se entienda.
    Muchísimas gracias desde ya.

    ResponderEliminar
  46. Hola de nuevo Leandro, creo que voy a abandonar la idea del la interfaz para comunicación entre formularios y adoptar el mismo sistema que haces en este ejemplo. De por si ya estaba trabajando con formularios dialog (o modal, ¿es lo mismo no?), solo tengo que crear en Ciudades la propiedad que voy a pasar al form Clientes. Me inclino por esta solución. Saludos.

    ResponderEliminar
  47. hola Mariano

    aqui no se uso ese tipo de comunicacion porque el form de busqueda se abria de forma modal, por lo que se debia cerrar el form hijo para poder continuar con al ejecucion del form padre, esto ahce que la comunicacion entre lso forma peuda realizarse de formas mas directa y simple

    Entiendo lo del Ciudades y Clientes pero lo interacion con el Form principal no cierra del todo, porque quierwes invocar un form que muestra ciudades sino va a devolver informacion ?

    mas alla de eso podrias simplemente no asignar nada en el owner por lo tanto al usar el "as" para castear comod evolvera null no invocar aningun metodo en el form padre que invoco al hijo

    saludos

    ResponderEliminar
  48. Hola Leandro, gracias por responder,Listo, ahora entiendo mejor porque no se uso ese tipo de comunicación.
    Con respecto a lo de llamar a Ciudades y no devolver informaciòn es porque en mi proyecto uso el mismo formulario Ciudades tanto para elegir una ciudad y devolverla a Clientes como para ABM de ciudades. Todo en el mismo formulario. Por eso si el usuario desea solo hacer ABM de ciudades accede desde el formulario principal.

    Cito: "mas alla de eso podrias simplemente no asignar nada en el owner por lo tanto al usar el "as" para castear comod evolvera null no invocar aningun metodo en el form padre que invoco al hijo"

    Si no entiendo mal me decís que en lugar de hacer esto:
    Dim FormCiudades As New frmABMCiudades
    FormCiudades.ShowDialog(Me)

    Haga esto:
    Dim FormCiudades As New frmABMCiudades
    FormCiudades.ShowDialog

    Si es así, yo lo intenté, pero en el evento closing de frmABMCiudades estoy haciendo esto

    If (Me.Owner.GetType Is GetType(frmABMClientes)) Then 'Han llamado desde el Formulario de tipo frmABMClientes

    'Paso a travez de la interface la ciudad seleccionada
    Dim Ciudades As New ClientesBO
    Dim _formInterface As ComunicacionI = CType(Me.Owner, ComunicacionI)
    _formInterface.ComunicaString(dgvCiudades.Item(1, dgvCiudades.CurrentRow.Index).Value())

    End If

    Yo necesito, y lo hago en la linea del If, consultar si o si quien es el owner de la instancia de frmCiudades, porque así como la invoca Clientes, lo podía hacer frmProveedores y también lo hace frmPrincipal. A veces devolviendo y a veces no un resultado.
    Esto tiene el problema que me dice que si no tiene owner me tira error, ¿puedo consultar si el owner es null? o ¿no existe?.
    Esta consulta ya es mas por curiosidad que otra cosa, ya que evaluando un poco el articulo actual me incline por crear propiedades en la clase frmCiudades que modifiquen su comportamiento, es decir que establezcan si se tiene o no que devolver un valor, luego, como en el articulo presente en el padre consulto por el resultado de dialog.

    ResponderEliminar
  49. hola

    no necesitas hacer esto
    If (Me.Owner.GetType Is GetType(frmABMClientes)) Then

    podrias usar

    Dim _formInterface As ComunicacionI = TryCast(Me.Owner, ComunicacionI)
    If _formInterface IsNot Nothing Then
    _formInterface.ComunicaString(dgvCiudades.Item(1, dgvCiudades.CurrentRow.Index).Value())
    End If

    como veras con el trycast puedes evaluar si convierte al tipo que defines
    saludos

    ResponderEliminar
  50. Si Leandro, acabo de probarlo y me funciona de maravilla el TryCast, no lo conocía. Muchas gracias por la ayuda.

    ResponderEliminar
  51. hola Fritz

    se me habia olvidado responderte

    Guía Arquitectura N-Capas DDD .NET 4.0

    esa guia esta muy buena
    saludos

    ResponderEliminar
  52. Muchas gracias Leandro! Saludos desde Pucallpa - Perú.

    ResponderEliminar
  53. Hola Leandro muy buenos artículos los 3 sobre el desarrollo en capas.

    Solo tengo una duda para que recuperas el id a la hora de insertar el registro? yo entiendo que lo haces para poder retornar toda la entidad, pero me gustaría que me expliques cual es la finalidad de capturar el id a la hora de hacer el insert.

    ResponderEliminar
  54. hola Cristian

    lo que sucede es que se usan campo del tipo identity, por lo que solo al insertar es que conoces el id que se genera

    la idea es poder devolver la entidad con este dato nuevo para poder ser usado (si se necesita) desde el negocio o la presentacion para informar al uusario que id o codigo se dio de alta

    es solo para devolver a modo informativo el id generado en la operacion

    saludos

    ResponderEliminar
  55. Hola Leandro. He leído brevemente tu aplicación y me surgieron dos preguntas:
    1) Veo que en tu capa de datos haces una conexión a la base de datos en cada consulta. Quería saber tu opinión de abrir la conexión una sola vez cuando se crea la clase.
    2) Tu base de datos y app_config están en la capa de presentación. He visto en otros proyectos que la agregan en la capa de datos. Quería saber que diferencia hay en hacerlo así.

    Muchas Gracias!

    ResponderEliminar
  56. hola Arévalo

    1- algo que debes conocer es que ado.net administra internamente un pool de conexiones, al cerrar desde codigo la conexion al salir del using no es que se cierra realmente, ado.net la mantine vigente un tiempo adicional por si se vuelve a invocar, es por eso que no se necesita de ninguna clase que abra globalmente una conexion y se use solo esa, ado.net lo resuleve por nosotos

    Agrupación de conexiones en SQL Server (ADO.NET)


    2- las aplciaciones web proveen un lugar para alojar la base de datos que se adjuntan dinamicamente al servicio de sql server
    otro tipos de desarrollo como ser lo de escritorio no tienen esta capacidad por eso lo dejo en la capa que pertence y con un Build Event se podria crear una copia a el \bin\Debug
    o si se deja en el proyecto de UI se copiara solo

    saludos

    ResponderEliminar
  57. Te felicito con por el artículo.
    Quería saber si en la capa de negocio está bien incrustar sentencias SQL o todas las sentencias SQL deberían estar definidas en la capa de datos (DAL).
    Porque a mi parecer incluir sentencias SQL en la capa de negocio (BL) no es correcto, pero es ahí donde tengo la duda.
    Gracias por tu articulo y tu ayuda.



    Muy buen articulo, te felicito.

    ResponderEliminar
  58. hola Diego

    es tal cual como lo mencionas, en la capa de negocio no puede haber sql, tampoco referencias a ninguna dll referida al accceso a datos

    en la capa de negocio no se puede usar clases que tengan que ver con ado.net

    saludos

    ResponderEliminar
  59. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  60. hola Gabriel

    algo que no has mencionado es como se relaciona un contrato o la linea del contrato con las otras entidades como ser concepto y recurso

    porque en el grid vas a mostrar conceptos y recursos, pero entonces que tienen que ver las listas del contrato ?

    las relaciones que comentas estan correctas, pero te encuantras con una limitacion del propio control DataGridView, este no muestra entidades complejas

    es por eso que podrias crear una clase que sea solo usada en la vista para convertir tu entidad de negocio en una de presentacion, o sea definir una clase que solo tenga propiedades simples para convertir la entidad compleja que recuperas del negocio y la conviertas en la simple para poder asignar al control grid

    saludos

    ResponderEliminar
  61. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  62. hola Gabriel

    entiendo la estructura que tienen las entidades, pero no me queda claro el problema que planteas y lo que mencionas de la reutilizacion

    lo que mencionaba es que para mostrar info en el grid debes aplanar la informacion ya que algo jerarquico no puedes asignarlo

    pero la nueva clase que definas seguro duplica datos porque un concepto tendra varios recursos

    quizas sea un tema para tratar en el foro, porque puede que sea algo extendo y necesites poner codigo para analizar el problema

    foro c#

    saludos

    ResponderEliminar
  63. Muy bueno Leandro, yo lo actualice al vb.net 2010 y agregue el modelo.emdx en la capa de negocio. me interesa lo de repositorio. ya que tengo un desarrollo en DDD usando dominio.

    ResponderEliminar
  64. hola gabymonte

    pero en el caso del articulo no se usan repositorios

    pero me parecio raro lo que comentas del edmx para el negocio, no seria mas que nada para la persistencia?

    saludos

    ResponderEliminar
  65. Hola Leandro, te hago una consulta.
    Tengo que implementar el patrón singleton y estoy haciendo un ejercicio basado en tu ejemplo. Las únicas instancias que veo en el ejemplo son hacías la capa Entities.. es ahí donde debo implementar el patrón singleton?

    ResponderEliminar
  66. hola

    no para anda, las entidades no pueden aplicar el patron singleton

    quizs podrias aplciarlo en la persistencia o en el negocio, pero si vas a hacer esto es mejor definirlo como static

    no veo que singleton aplique en un modelo en capas

    saludos

    ResponderEliminar
  67. Hola, Excelente Blog.
    Quisiera que me ayudaras con unas dudas:
    Estoy por desarrollar un sistema de Solicitud de adelanto de gastos, aprobación de viajes, requisiciones (consumibles y activos fijos)
    e hice la base de datos que son como 14 tablas (que se interrelaciona en tre si) y quiero desarrollarlo en capas, pero tengo la duda
    de si la cantidad de DB es la equivalente para las clase?

    La capa de presentacion puede ser hecha con form? Y como seria para las capas de negocio y de acceso de dato?

    ResponderEliminar
  68. hola Weimen

    como es eso de la cantidad de DB, sera la cantidad de tablas
    si bien hay una correspondencia entre una tabla y uan clase, no es una regla, puede haber clases que mapean a mas de una tabla, esto se da sobre todo si implementas herencia

    la capa de presentacion es la que interactua con el usuario, seguro sera un form

    como sera el negocio o datos, la verdad no se, eso depende mucho del dominio de tu aplicacion

    saludos

    ResponderEliminar
  69. Hola, gracias por responder.
    Y de casualidad no tendras algun ejemplo de: clases que mapean a mas de una tabla implementando herencia

    ResponderEliminar
  70. hola

    quizas este articulo ayude un poco

    n-Layer - SchoolManager - Herencia ...

    alli explico como definir herencia y persistirla

    saludos

    ResponderEliminar
  71. Hola Leandro, me gustaría saber porque en las capas de datos, entidades y negocios le asignas una clase a cada "ítem" de tu negocio. Es una forma de organizarte mejor? o estas aplicando una técnica? Gracias..

    ResponderEliminar
  72. hola Si Señor

    en realidad la asignacion no siempre es directa, por ejemplo analiza la entidad InvoiceLinesEntity, veras que para esta entidad no hay capa de negocio, ya que es la factura quien administra las lineas

    por lo general por cada entidad tendras su negocio y persistencia, pero no es una regla exacta

    alli estoy modelando algo basico, el negocio expone funcionalidad que le UI requiere puede coincidir o no con las otras capas

    saludos

    ResponderEliminar
  73. Hola Leandro.

    Tengo una web en la que obtengo los datos de un Datos.xsd y luego trabajo con ellos en cada uno de los WebForms, no trabajo ni con entidades, esto es realmente un CAOS

    Estoy trabajando con WebForm, supongo que es totalmente válido también este modelo para poder aplicarlo en mi proyecto ¿no?, ya que prefiero adaptarlo de esta manera que tener que usar(aprender,aplicar) MVC ASP.NET.

    Dejando mi proyecto web tal y como está (UI) y creándome las bibliotecas de clases de Lógica de Negocio, Acceso a Datos y Entidades y realizando las referencias que indicas ¿no?

    Y una segunda duda.
    He visto que algunos ejemplos usas una excepción propia BusinessException, ¿ésta sería una manera correcta de crearla?
    http://www.codeproject.com/Tips/90646/Custom-exceptions-in-C-NET
    ¿Sería mejor crear una excepción por cada BO?

    Me están sirviendo de gran ayuda tus ejemplos, muchas gracias por todo el aporte que proporcionas.

    UN SALUDO!

    ResponderEliminar
  74. hola misretoques

    el modelo que aqui explico es completamente valido para aplicarlo en un desarrollo web
    solo cambiarias la UI, el resto de las capas se conservan identicas

    Es correcto crear un exception como la del articulo, pero solo crea una sola, despues por cada error que se genere le cambias el mensaje que devuelve

    saludos

    ResponderEliminar
  75. Hola, muy claro el ejemplo, la verdad que ayuda mucho a entender la interaccion entre las capas. Simplemente tengo una duda con respecto a este tema. Si se tuviera que validar que el usuario no ingrese cantidades con valores negativos o cantidades mayores al stock actual de productos. Donde deberia ubicarse esa validacion? En que capa corresponderia? Sería en la de presentación? Gracias de antemano por la ayuda. Saludos!

    ResponderEliminar
  76. hola Tomas

    lo que planteas veo dos validaciones, un stock negativo y otroa que no supere el stock existente
    la primera se podria realizar en la presentacion verificando que no se ingrese un valor menor a cero, pero tambien deberias realizando nuevamente en la capa de negocio

    la del stock superior al existente eso claramente va en la capa de negocio, ya que deberias recuperar el producto para conocer su stock y luego validar

    en resumen todas las validaciones van en la capa de negocio, pero aquellas simple que puedes realizar tambien en la presentacion, en esta se ponen solo validaciones simples como ser tipos de datos, validacion por rango, etc, pero todo despues se valida en el negocio nuevamente

    saludos

    ResponderEliminar
  77. Hola Leandro.
    Me ha surgido otra duda. Quiero capturar las excepciones que puedan surgir en la capa de Acceso a Datos. Y también por ejemplo las que puedan surgir en la Lógica de Negocio.
    Mi duda es, lo habitual que sería, ¿crear una excepción propia para DAL y otra para BO?, las excepciones que debería crear ExceptionBO en la biblioteca de clases BO y ExceptionDAL en la de DAL?
    ¿Cómo se suele realizar el tratamiento de errores para N-Tier?

    Un saludo y gracias.

    ResponderEliminar
  78. hola misretoques

    no creo que haga falta diferenciar con custom exception los errores de DAL o BO, podrias simplemente definir excepciones tecnicas y de negocio, entonces las de acceso a datos podrian ser consideradas tecnica

    http://social.msdn.microsoft.com/Forums/en-US/vcses/thread/31410a75-d0a3-4264-b910-324fc8a576d6

    saludos

    ResponderEliminar
  79. Hola, tu ejemplo dice N-Tier - Desarrollo en capas, pero N-Tier con capas no son lo mismo, de hecho podrían trabajar juntos, tu desarrollo es N-Layer (N-Capas). N-Layer o N-Niveles se refiere a la separación física de tu lógica de aplicación, esto se podría hacer, por ejemplo, con WCF, teniendo tu presentación (cliente) consumiendo tus servicios que se encuentran en otro servidor físicamente separado.
    Saludos cordiales

    ResponderEliminar
  80. Hola Leandro.

    Tengo una pregunta, si yo quisiera implementar un boton Eliminar Detalle, por ejemplo agrego un producto a la grilla detalle pero me equivoque y quiero eliminar esa linea de factura como lo haría por que cada ves que quiero eliminar me aparece un error que dice "el indice esta afuera del intervalo, tiene que ser un valor no negativo e inferior al tamaño de la coleccion", intente de varias formas pero siempre me sale el mismo error.

    ResponderEliminar
  81. hola Sergio

    recuerda que debes enviar a la capa de negocio el id de la entidad que estas mostrando en el grid, y no el index de la row del control grid

    tambien valida que se este seleccionando un row del grid cuando presionas el boton de eliminar

    si detectas que

    if(datagridview1.CurrentRow == null){
    //aqui informas que no hay una fila seleccionada
    return;
    }

    o sea informas y no continuas
    saludos

    ResponderEliminar
  82. Hola Leandro.

    Estoy trabajando con Web Form (vs 2010, c# asp.net 4.0) y el llamado código spaguetti. Me he propuesto separar mi código en capas para que sea más legible y mejor escalable..

    En mi código tengo un DataSet.xsd en el cual tengo las tablas con DataTable y luego cada una de ellas tiene sus TableAdapater que llaman a procedimientos almacenados de mi BD.

    Tenía un fichero Logica.cs donde tenía los métodos a los que llamaba desde las distintas páginas.

    Ahora he separado como has indicado
    Entity -> donde he metido mi DataSet1.xsd
    DAL -> donde uso los DataTable y DataAdapters de la capa Entity para acceder a la BD
    BL -> donde llamo a los métodos de DAL
    Web -> donde llamo a los métodos BL

    Mis dudas son:
    1. ¿Está bien separado el Entity poniendo el DataSet1.xsd ahí?
    2. En el DAL estoy llamando a los DataAdapters, ¿sería mejor quitarlos del Entity y en la capa de acceso a datos llamar así a los métodos?:

    DataSet1.partidosDataTable dt = new DataSet1.partidosDataTable();

    using (SqlConnection conn = new SqlConnection(connectionString))
    {
    using (SqlCommand cmd = new SqlCommand("partidosSelectProximos", conn))
    {
    cmd.CommandType = CommandType.StoredProcedure;

    conn.Open();

    SqlDataAdapter da = new SqlDataAdapter(cmd);
    da.Fill(dt);
    }
    }
    return dt;


    O mejor seguir usándolo:

    partidosDataTable adaptador = new partidosDataTable();
    DataSet1.partidosDataTable datos = new DataSet1.partidosDataTable();
    adaptador.FillProximos(datos);
    return datos;



    Muchas gracias y un saludo.

    ResponderEliminar
  83. hola misretoques

    la verdad recomendaria que en lugar de dataset tipados uses entidades con clases

    aunque podrias analizar la parte 2 del articulo, alli uso dataset en capas

    en realidad usarias los dataset tipados para definir las entidades pero estas no tendrias logica de persistencia, o sea no se usarias los tableadapter porque no se puede separar uno de otro

    saludos

    ResponderEliminar
  84. Hola Leandro.

    De momento me gustaría realizarlo poco a poco, aunque eso me lleve el doble de trabajo.
    Por lo que mi intención es usar DataSets. Para saber si te entendido bien, lo que me comentas es que 'sí' que debo separar la lógica de persistencia (TableAdapters) de los DataSet (Entities), ya que si dejo los TableAdapters en Entities se podría acceder a la lógica de persistencia desde la WEB al realizar una instancia de un DataTable. ¿no?

    Por lo que me debería quedar así:
    Entity -> DataSet1.xsd (Sin TableAdapaters)
    DAL -> Métodos estáticos donde llamo a los procedimientos almacenados de la BD de esta manera:
    DataSet1.partidosDataTable dt = new DataSet1.partidosDataTable();

    using (SqlConnection conn = new SqlConnection(connectionString))
    {
    using (SqlCommand cmd = new SqlCommand("partidosSelectProximos", conn))
    {
    cmd.CommandType = CommandType.StoredProcedure;

    conn.Open();

    SqlDataAdapter da = new SqlDataAdapter(cmd);
    da.Fill(dt);
    }
    }
    return dt;
    BL -> donde llamo a los métodos de DAL
    Web -> donde llamo a los métodos BL

    Un saludo y muchas gracias por la información compartida. GRACIAS.

    ResponderEliminar
  85. hola Unknown

    exacto ese es el problema, los datatable y tableadapter que estan en un dataset tipado no se pueden separar, por lo tanto si defines un dataset tipado como modelo de entidades y este lo referencias en la UI habilitarias a que desde alli se usa el tableadapter directo, lo cual rompe con al separacion en capas

    el resto del codigo es correcto, defines dataset sin tableadapter, de esta forma puede separar las responsabilidades y crear tu propia persistencia

    saludos

    ResponderEliminar
  86. Hola Leandro.

    En el DataAccess estás usando:
    using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))

    ¿Se podría hacer de la siguiente manera?

    public static class ClassDAL{
    private static string connectionString = ConfigurationManager.ConnectionStrings["default"].ToString();

    ...
    public void MetodoDAL(){
    using (SqlConnection conn = new SqlConnection(connectionString))){...}
    }
    }

    ¿Puede ser static el string de la conexión?, ¿aconsejas ponerle o no al string static?


    Muchas gracias por el artículo me ha servido mucho de ayuda.

    ResponderEliminar
  87. hola Unknown

    si se podria hacer lo que planteas

    aunque la asignacion seria mejor si la realizas usando

    Constructores estáticos (Guía de programación de C#)

    o sea defines la variable static pero la asignacion del dato desde el ConfigurationManager la realizas en el constructor static

    saludos

    ResponderEliminar
  88. Hola tuttini gracias por tu aporte, pero hay manera que hagan un tutorial parecido pero donde empieces de una vez en capas. La verdad es que así no entendí nada. no se si usas algunas referencias que no dices pq no me corrió el programa.pero igual se me hace complicado como lo haces todo revuelto, luego sacas una capa y después la otra. de llegar a entenderlo me tocaría hacerlo igual que tu tuto, hacer la api completa como siempre y luego ir sacando capas. La idea seria aprender a hacer un desarrollo en capas así sea básico pero desde cero. Te agradezco si lo tienes en cuenta, ando muy interesado en aprender pero ese método que usas no lo entiendo. no he podido avbanzar

    ResponderEliminar
  89. hola CRISTIAN

    pero este articulo si aplcia capas desde el inicio, los otros dos son opcionales para poder ver la transformacion, peor los anteriores no son necesarios analizalros sino lo necesitas

    veo que comentas que el programa no corre, estas obteniendo algun mensaje de error en concreto ?

    a que llamas api completas ?

    puedes definir algun parte en concreto que no se entendendio? es la persistencia, o quizas la comunicacion entre las capas?
    o sea hay algun punto en concreto que causa duda he impide entender

    saludos

    ResponderEliminar
  90. Es como inicias, en este articulo inicias con este codigo if (cliente == null)
    cliente = new CustomerEntity();

    cliente.FirstName = txtNombre.Text;
    cliente.LastName = txtApellido.Text;
    cliente.Company = txtCompañia.Text;
    cliente.Address = txtDireccion.Text;
    cliente.Email = txtEmail.Text;

    cliente = CustomerBO.Save(cliente);

    Pero no tengo idea donde se pone para poder empezar. Otra duda que tengo es como empiezas? creas una solucion vacia y vas agregando proyectos o crear lo normal la aplicacion en winforms y agregas los proyectos de librerias de clases?? hay alguna diferencia?. Otra cosa, en lo del ejemplo me refiero con que solo pongas un pequeño ejemplo donde se inserte un cliente en la base de datos y luego se consulte y en capas ya podria entender. Se me hace muy complejo el articulo.

    En este articulo explican la programacion en capas de un modo en que lo entendi http://dotnetfyi.wordpress.com/2009/08/14/desarrollo-de-aplicacion-en-capas-con-visual-studio-2008/ explica desde lo mas basico asi pude arrancar pero me genera un error con el datetime, hay quede parado, me indicaste que esta mal la parte de insertar datos pq no usa parametros pero para entender lo de capas me sirvio mucho. Te agradesco si me puedes colaborar para aprender el desarrollo en capas que es muy importante

    ResponderEliminar
  91. hola CRISTIAN

    puedes empezar analizando desde uno de los extremos, alli veo que muestras codigo de la presentacion, por lo que si vas navegando la funcionalidad podrias ir de alli al Save() de la capa de negocio y luego de alli a el codigo de la persistencia, es una secuencia de llamadas que pasa de una capa a otra y cada una agrega su funcionalidad, no se si has estudiado redes, es igual al Modelo OSI

    puedes empezar de ambas formas, con una solucion vacia y vas a agregando proyecto o empezar creando el proyecto de presentacion y a este le agregas las dos o 3 proyectos Class library para el resto de las capas, ambos caminos son validos

    si el ejemplo de ese articulo resulto util toma los conceptos de alli y solo cambia la parte donde define la query usando parametros y solo esto
    por lo que vi no difiere mucho a lo que planteo solo que en mi ejemplo es algo mas grande, pero creo que si emepiezas en una capa y los vas navegando por los metodos deberias poder entenderlo

    saludos

    ResponderEliminar
  92. Hola, me fue util pero tengo un error, dice que el formato de la fecha no es valido, si me puedes colaborar te lo agradesco.

    Volviendo al tema de crear el proyecto, Cual de las 2 formas es la mejor?? la recomendada?? Creando el proyecto vacio y agregar los proyectos o crear el proyecto de interfaz y agregar los proyectos?? Me gustaria aprender de la forma correcta, por eso estoy tratando de entender tu tutorial pero me ha resultado dificil. Igual ya lo he leido varias veces y no parare hasta entenderlo ya que es muy completo tu tutorial

    ResponderEliminar
  93. Hola Leandro. primero quiero dar gracias por tus grandes aportes me han servido mucho. Me he basado en esta arquitectura de capas para generar un proyecto para Winform. Ahora debo hacer otro en ASP MVC basandome en las mismas capas. La duda que tengo es al crear el modelo de una pagina para mostrar por ejemplo los datos de un Proveedor. Veo que en el modelo debiera usar la misma estructura que he creado en Entity (capa). Como se debiera manejar esto, o algún enlace donde encontrar mas informacion.. Muchas gracias!

    ResponderEliminar
  94. hola Marcos

    bueno en este punto no hay una ciencia exacta, hay quienes te diran que uses la clase que defines en entity como modelo de la view (esto lo veras mucho en ejemplos con entity framework) y hay quienes te diran que debes crear una clase Model que hagas mapear contra la que defines en el dominio

    ambas opciones son validas aunque personalmente prefiero definir un Model separado de la entidad y usar automapper para convertir de uno a otro en la capa de servicio

    entonces podrias tener entidades que definen tu dominio y entidades de presentacion que usas en la view

    la capa de aplicacion o servicio separa ambos mundos, usando automapper para convertir de uno a otro

    saludos

    ResponderEliminar
  95. Hola Leandro.
    Una duda respecto a las N-Capas.
    Esas n-capas entiendo que cada una puede ir alojada cada una de ellas en un servidor distinto ¿no?
    Mi pregunta sería:
    ¿Estaría bien crear un WebService, por ejemplo para llamar a métodos de la BL, y otro WebService en DAL que sólo lo pueda llamar la BL, y la parte de presentación ya podría ser dispositivos Móviles, App escritorio, Web... que llamen al WebService de la BL.?

    ¿Qué opinas?

    Un saludo, gracias por el tuto.


    ResponderEliminar
  96. hola Unknown

    yo aconsejaria que con solo hacer remota la fachada de negocio alcanza y sobra
    no veo que sea necesiria otro servicio para los datos, eso estaria de mas

    con solo exponer mediente servicio el negocio alcanza
    si te animas para los servicio utiliza WCF o Web API

    saludos

    ResponderEliminar
  97. hola leadro muchas gracias por el codigo . pero me ah salido un error como hago si tengo un campo con valor null . ya no me quiere cargar la grilla estoy trabajando en vb.net

    ResponderEliminar
  98. Hola Leandro, estoy siguiendo tu post de N-Tier, y ya tengo montado el proyecto, pero me ha surgido una duda.

    Tengo varias entidades relacionadas entre ellas y lo he montado siguiendo N-Tiers como indicas en el post

    Tengo:
    PedidoEntity -> Que tiene List()
    DetallesEntity -> Que contiene ArticuloEntity
    ArticuloEntity -> Que tiene List()

    El ejemplo también es bastante simple, pero ésto puede crecer todo lo que se quiera.
    Cuando obtengo un PedidoEntity tengo un PedidoDAL.GetAll() éste recupera PedidoEntity sin relaciones, he pensado en crear un PedidoDAL.GetAllWithRelationship() que obtenga las relaciones que tiene con las otras entidades, pero éstos a su vez traeran sus relaciones...y en el caso que sean muchos datos puede tardar mucho en cargar datos.

    ¿Cómo podría resolver éste problema?, he leídojavascript:void(0) que en ASP.NET 4.0 trae la clase System.Lazy, ¿ésto me podría ser de ayuda?, en el caso que sea sí, ¿Dónde debería poner el Lazy en DAL o en el Entities?

    Muchas gracias Leandro.

    ResponderEliminar
  99. hola luis

    pero ese campo null es un valor que recuperas o lo envias para filtrar los datos como parametro de una query ?

    sino quieres filtrar simplemente podrias validar si ese valor llega en null y no haces nada

    ahora si es un valor que recuperas podrias tambien verificarlo usando

    If reader("nombrecampo") IsNot DBNull.Value Then
    'aqui accedes al campo
    End If


    saludos

    ResponderEliminar
  100. hola Unknown

    no evaluaste usar un ORM, como ser Entity Framework ?
    con este temas de lazy load son muy simple de implementar

    hacerlo a mano lo que planteas no hay muchas alternativas mas que la que comentas de crear un metodo Getall() que retorne la entidad simple y otro que retorne la entidad y sus relaciones

    aunque si podrias analizar

    n-Layer - SchoolManager - Herencia y navegación de entidades relacionadas (2/2)

    con linq esto podria ser mas simple
    saludos

    ResponderEliminar
  101. Hola Leandro.

    Estoy Aplicando tu Ejemplo con una base de datos MySQL con la conexion no hay ningun problema y extraccion de datos tampoco, el problema esta al momento de mostralos en un datagridview tengo una tabal Area de 2 campos el id y el Area propiamente dicha, pero en la grilla no se muestra el id, ya puse las propiedades de bindeo pero nada
    que podria estar fallando gracias por la respuesta y el tiempo.

    ResponderEliminar
  102. hola Sr. Hueveras

    entiendo que las columnas las defines en tiempo de diseño y especificas el DataPropertyName de cada columna
    defines en la del area el nombre de la propiedad, pero ojo tiene que se de tipo simple, ya sea int, string, etc si defines la propeidad de un tipo complejo no vas a verla correctamente

    sino podrias dejar que las columnas se creren de forma dinamica para ver que aparece, solo no definas el AutoGenerateColumns = false, sino lo pones entonces se crearan las columnas para ver que nombres se crean

    saludos

    ResponderEliminar
  103. Hola Leandro, he separado mi proyecto como indicas en tu ejemplo.

    Estoy intentando usar TransactionScope, lo que quiero hacer que cuando alguien vaya a empezar un pedido, inicializar un pedido y asignárselo a una cesta nueva. Lo que quiero que suceda es que si por cualquier cosa al insertar la cesta falla haga un rollback y el pedido no se inserte.
    ¿Ésto podría hacerse?

    using(TransactionScope scope = TransactionScope())
    {

    PedidoEntity pedido = new Pedido(){...};
    int pedidoId = PedidoDAL.Save(pedido);

    cesta = new CestaEntity(){
    PedidoId = pedidoId,
    UsuarioId = -1 //intento generar un error
    }
    CestaDAL.Save(cesta);
    scope.Complete();

    }

    Si ejecuto esto en la capa de negocio, me inserta el pedido, y cuando va a insertar la cesta falla, y se queda insertado sólo el pedido.

    ¿Ésto podría controlarlo?

    Indicar que en el DAL, estoy llamando a Procedimientos Almacenados de mi BD.

    Un saludo y gracias

    ResponderEliminar
  104. hola misretoques

    pero si genero un error se supone que no deberia pasar por el scope.Complete()

    dentro del Save() de la DAL defiens algun try..catch ? porque recuerda que deberias lanzar ese error hacia fuera para que salte el complete

    saludos

    ResponderEliminar
  105. Hola Leandro ya no está el código para descargar, donde lo puedo conseguir.

    ResponderEliminar
  106. hola buenas noches queria descargar el codigo en c# [csharp]Chinook3Capas.zip
    pero no esta el link disponible excelente aporte genio...

    ResponderEliminar
  107. hola Juan

    ya estan los links actualizado

    salydos

    ResponderEliminar
  108. Una pregunta leandro,

    en el caso de usar un data binding como hiria la distribucion en el procedimiento n-tier

    y otra cosa mas si ya al realiza la aplicacion me marca errores de dependendia de la aplicacion los dlls que te genera donde hirian instalados ya que la aplicacion estaria redidente en un servidor

    ResponderEliminar
  109. Justamente estoy buscando algun software o Hanger factory que sea util en mi negocio

    ResponderEliminar
  110. Leandro como estas, agradeciendote de nuevo, queria molestarte con algo que me preocupa para una aplicacion que estoy desarrollando y es la inyeccion de codigo sql, ya valide caracteres especiales pero pues viendo bien una inyeccion podria ser "select ciudad from pais" como podria implementar la restriccion para inyeccion de codigo te agradezco de antemano

    ResponderEliminar
  111. hola ksoto

    no entendi, a que llamas inyeccion de sql ? definir un SELECT completo no es inyeccion

    imagino apuntas a la vulnerabilidad de Sql injection pero esta ahce referencia al uso de parametros en las queries a sql

    saludos

    ResponderEliminar
  112. Hola, me parece excelente tu trabajo Leandro.
    Quisiera saber como sería este ejemplo si dejáramos solo con tres capas, sin entidades(presentación, negocio y datos). Se podría colocar en vez de entidades, propiedades publicas dentro de datos?. Cómo seria el ejemplo completo.
    De antemano Gracias.

    ResponderEliminar
  113. hola Capitan

    o sea quieres aplicar el patron Active Record ?

    Active Record pattern

    con este podria hacer que la entidad tenga las operaciones de acceso a datos

    saludos

    ResponderEliminar
  114. Leandro excelente Blog, Tengo una pregunta si la aplicación a desarrollar(software administrativo) es demasiado grande, Que sería más conveniente ? 3 proyectos(las 3 capas) POR cada modulo(Bancos, Clientes, Proveedores, Produccion, etc.) ya que pienso que si hago 1 sólo proyecto de capa de presentación para todo el sistema el ejecutable sería demasiado grande y no creo que sea muy conveniente para hacer actualizaciones, o que me puede recomedar al respecto.
    Gracias de antemano.

    ResponderEliminar
  115. hola Ignacio

    cuando creas proyectos para cada modulo por lo general se piensa en la capa de negocio, datos, entidades, no tanto en la de UI

    quizas lo mas simple seria separa los modulos con carpeta en el proyecto de presentacion

    hace mucho se usaba algo como esto
    Smart Client Software Factory 2010
    pero creo que quedo algo obsoleto
    ya que actualmente la tendencia es usar WPF con algun framework como ser Prism

    saludos

    ResponderEliminar
  116. Buenos Días:
    Muchas gracias por el Articulo, de mucha orientación y gran ayuda; una consulta quise adjuntar la base de datos a mi servidor, para poder realizar las pruebas, pero no me deja. Que versión es o si se tiene el BAK de dicha Base de datos, en mi caso estoy usando el SQL Server 2012.
    Gracias por el apoyo..

    Atte
    Jorge Diaz

    ResponderEliminar
  117. hola leandro, con esta metodologia que recomientas, una aplicación con este estilo llamando a base de datos o usando un ORM, bien sea nhibernate o EF, y es que estoy haciendo algo para un consultorio odontologico, y no se si utilizando EntityFramework con MVC 5 con repository o facade y en VS2013 sea muy robusto, yo catalogo un proyecto mediano que podria expandirse. que recomendarias?

    ResponderEliminar
    Respuestas
    1. hola
      Porque dices que usar EF con respository en mvc 5 no sea robusto ? lo que describes es una buena combinacion
      usar un ORM con mvc 5
      lo que planteas es recomendable, por supuesto si puedes ademas unir un IoC como ser unity, ninject, etc para poder inyectar al controller el servicio y los respositorios seria aun mejor, de esta forma podrias implementar test en el codigo
      saludos

      Eliminar
    2. hola leandro!! bueno leandro a lo que me referia era que SI usar Esta metodologia Sería buena recomendación, para el proyecto que te mencione! y digo que si esta metodología es MEnos robusto que un ORM o en que se diferencia con Usar un ORM?

      Eliminar
    3. hola
      El tema es que no se si robusto seria la palabra que se deba usar, como en todo los casos hay pros y contras aplicando una tecnica u otra
      Con un orm la generacion de consultas es mas simple y puedes aplicar POO facilmente definiendo entidades, por supuesto pierdes algo del control de la db pero en casos puntuales podrias mapear storedprocedure
      Creadno el codigo a msno seguro sera mas trabajoso, y escribiras mucho mas codigo, pero bueno ganas en control ya que todo lo has escritu tu mismo
      Particularmente me inclino por usar un orm, hace la vida mucho ams simple.
      saludos

      Eliminar
  118. Muchas Gracias. Me sirvió muchísimo. Sos un ejemplo a seguir.

    Saludos.

    ResponderEliminar
  119. hola Leandro

    Muy buenos sus tutoriales, quisiera que me ayudara, tengo un error de compilación cuando se llama la cadena de conexión desde otra de las capas. Estoy trabajando con Access y la línea de código de error es:
    using (OleDbConnection cnx = new OleDbConnection(ConfigurationManager.ConnectionStrings["cnxString"].ToString()))
    me puedes ayudar que es lo que pasa ?

    ResponderEliminar
    Respuestas
    1. hola
      Que dice el mensaje de error ?
      Defines en el app.config el connection string con la key cnxString ?
      Puede que falle porque no esta pudiendo encontrar la key de configuracion en el .config
      saludos

      Eliminar
  120. Buenas Leandro.
    Implementé en mi proyecto con la estructura N-Tier, funciona correctamente, pero me surge una duda. Estoy implementando un apartado para estadísticas, y me gustaría obtener los datos vía JSON para usarlos con una api de gráficos. ¿Cómo debería realizar con la estructura de n-tier para obtener datos vía JSON?
    Un saludo y muchísimas gracias por la información que proporcionas, me eres de gran ayuda.

    ResponderEliminar
    Respuestas
    1. hola
      si usas json entiendo que estas desarrollando una aplicacion web, lo que no mencioans es si se trata de asp.net o asp.net mvc.
      El json seguro lo necesitas en la UI para poder aplicar al control de estadistica, en ese caso si tiene una clase o coleccion podrias serializarla a json usando la libreria json.net
      saludos

      Eliminar
    2. Si, estoy desarrollando una aplicación web (WebForms Framework 4.5.2), con las capas Entities, BLL y DAL y WEB, cuando necesito datos en la UI que no hacen referencia a una entidad, ya que son un conjunto de datos con importes totales y valores que no reflejan una entidad de mi sistema. Donde debería crear la llamada para devolver JSON?

      Por ejemplo, en una página quiero mostrar el numero de usuarios que entraron por móvil, tablet o pc

      {"info": {
      "pc":"120",
      "tablet":"20".
      "movil":"100"
      }
      }

      Eso es lo que yo quiero recibir en mi UI, pero quien es el que me devuelve el JSON? el BLL, el DAL?, dónde debo hacerl el serializado de json?

      Gracias.

      Eliminar
    3. Buenas Leandro, en el ejemplo de las N-Tier que has redactado, la devolución de JSON ¿cual es la capa encargada de devolver esos datos, la BLL?, la capa DAL me trae los datos y los almacena en un Entitie y los pasa a la capa lógica(negocio) y ésta debería ser la encargada de Serializarlo a JSON/XML/.. y mandarlo a la UI?

      Si no estoy en lo cierto corrigeme, no tengo claro bien este concepto.

      Un saludo y muchas gracias

      Eliminar
    4. hola
      el json lo devuelve siempre la capa de servicio, en tu caso seria la web. Las capas BLL y DAL solo conoces de entidades y objetos, es en la web que conviertes estos para adaptarlos al transporte ya que puede que quein los consume necesite que sean json,xml o algun otro formato
      Si usarias WebApi esto seria un concpeto transparente, desde asp.net clasico desde algun WebMethod definido en el aspx podrias devolver ese json como respuesta al cliente para consumirlo con ajax con jquery
      saludos

      Eliminar
  121. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  122. BUENOS NOCHES FORO .. AYUDA NECESITO IMPRIMIR FACTURA DESDE C# 2015 SIN CRISTAL REPORT... YA QUE HE TRATADO POR VIA DATASET Y DATATABLE LUEGO DE INSTAL EL SAP PARA VS2015 .. CUALQUIE AYUDA GRACIAS ... SI ES CASO ME BAJO LA VERSION A 2013???

    ResponderEliminar
  123. Hola Leandro.
    Mi proyecto está integrado con N-TIER, en la capa lógica de negocio (BLL) tengo métodos que lo usa tango una aplicación web admin como la aplicación web publica, hay alguna manera de separar los métodos para una y otra y a ser posible no tener que reescribir los que ya existen? o en este caso debería crear otra capa que fuese BLLPrivate y BLLPublic.., o algúna interfaz, alguna orientación? Gracias.

    ResponderEliminar
  124. Excelente informacion Leandro, muchas gracias !!!

    ResponderEliminar
  125. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  126. Hola. Muy buena la explicación pero no está disponible el link de esta parte ni de la anterior. Se me complica seguirlo. Gracias.

    ResponderEliminar
  127. saludos me gustaria porque me da error aqui: using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
    {
    conn.Open();

    string sql = @"SELECT Count(*)
    FROM Customer
    WHERE CustomerId = @customerId";

    SqlCommand cmd = new SqlCommand(sql, conn);
    cmd.Parameters.AddWithValue("customerId", id);

    o si hay que hacer una base de datos o que? por favor

    ResponderEliminar