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:

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.