In Part I, I introduced the concept of the Streetlight Store and the architecture. In this segment, we will walk through the creation of a data contract for products, interfaces for managing transactions, and a database-first Entity Framework implementation of the interfaces.
The initial plan was to distribute the store as a single library with the intent of using a SQL server database with a fixed schema. However, just as flexibility is important in the front end, it is also important in the back end so I decided to abstract the back-end logic. The namespaces listed in Part I (Streetlight.Store.Contracts and Streetlight.Store.DataAccess) have been separated into two assemblies. The DataManager class described in Part I has been abstracted into the Streetlight.Store.Contracts.ITransactionManager interface and the other DataManagers have been similarly abstracted into IProductManager, IAccountManager, IOrderManager, and IShipmentManager.
The Product Data Contract
As I mentioned in Part I, the Streetlight Store is centered around POCO classes. The first of these classes we are going to look at is the Product class. This class encapsulates all of the pertinent information about a product.
namespace Streetlight.Store.Contracts
{
using System;
public class Product
{
public string Id { get; set; }
public string Name { get; set; }
public decimal? Price { get; set; }
public string PriceText
{
get
{
return string.Format("{0:C}", Price);
}
}
public string PackagingTypeCode { get; set; }
public string GlobalId { get; set; }
public string Manufacturer { get; set; }
public string PartNumber { get; set; }
public float? Weight { get; set; }
public string Description { get; set; }
public string Status { get; set; }
public string ItemNumber { get; set; }
public float? Length { get; set; }
public float? Width { get; set; }
public float? Height { get; set; }
}
}
Database Schema
I’m sure some of you are asking, “Why database-first? Why not use code-first or model-first?” The short answer is I already have a database. While I’m sure it is possible to use one of the other techniques, simply generating the model from the database was the easiest approach.
CREATE TABLE dbo.Product
(
[ProductId] int identity(1, 1), -- Primary Key
[Name] varchar(MAX), -- Prouct name
[Price] decimal(18, 2), -- Price of product
[PackagingTypeCode] varchar(50), -- Packaging type for shipping
[GlobalId] varchar(MAX), -- Global Trade Item Number (GTID)
[Manufacturer] varchar(MAX), -- Manufacturer's name
[PartNumber] varchar(MAX), -- Manufacturer's part number
[Weight] real, -- Weight of product
[Description] varchar(MAX), -- Product description
[StatusId] tinyint, -- Status ID
[ItemNumber] varchar(MAX), -- Internal unique identifier
[Length] real, -- Length of product
[Width] real, -- Width of product
[Height] real, -- Height of product
CONSTRAINT Product_PK PRIMARY KEY CLUSTERED
(
ProductId
)
)
go
Notice that not everything is an exact match between the database schema and the object model. Strings are used instead of numeric types in the object model to allow flexibility in the back end. Also, the naming convention of the object model is not identical to the database schema. For example, the ProductId database field maps to the Id property of the object model. A string is used in the object model instead of an integer so that GUIDs and other non-integer and non-numeric primary keys are supported.
Interfaces or “Operation Contracts”
Now that we have our database and object model, we need to be able to do some basic CRUD operations. Following the paradigm established in Part I, the ITransactionManager interface contains properties for the other manager interfaces. The other interfaces define the operations that can be performed following the “operation contract” paradigm from WCF. For now we will focus just on the IProductManager and ITransactionManager. These interfaces loosely follow unit of work pattern (ITransactionManager) and repository pattern (IProductManager).
namespace Streetlight.Store.Contracts
{
using System;
public interface ITransactionManager : IDisposable
{
IProductManager Products { get; }
}
}
In the following code for IProductManager, four methods perform CRUD transactions while the remaining two (DuplicateItemNumberExists, DuplicateGlobalIdManufacturerExists) can be used for validation.
namespace Streetlight.Store.Contracts
{
using System;
using System.Collections.Generic;
/// <summary>
/// Provides an interface for managing product data.
/// </summary>
public interface IProductManager
{
/// <summary>
/// Returns a list of all products.
/// </summary>
/// <returns>New instance of List<Product> containing data for all products</returns>
List<Product> ListAll();
/// <summary>
/// Stores the provided Product as a new record.
/// </summary>
/// <param name="product">Product to be saved</param>
void CreateNew(Product product);
/// <summary>
/// Checks to see if a product exists with the specified item number and different
/// ID. This function can be used to ensure exclusivity of item numbers. Returns true
/// if a product exists with the specified item number and different ID, otherwise
/// returns false.
/// </summary>
/// <param name="id">ID of item to validate item number for. If parameter is null,
/// empty, or whitespace, all existing items will be searched.</param>
/// <param name="itemNumber">Item number to check for duplicates</param>
/// <returns>True if a product exists with the specified product number and different ID,
/// otherwise false.</returns>
bool DuplicateItemNumberExists(string id, string itemNumber);
/// <summary>
/// Checks to see if a product exists with the specified global ID and manufacturer and
/// different ID. This function can be used to ensure exclusivity of Global ID/
/// Manufacturer combinations. Returns true if a product exists with the specified
/// global ID and manufacturer and different ID, otherwise returns false.
/// </summary>
/// <param name="id">ID of item to validate item number for. If parameter is null,
/// empty, or whitespace, all existing items will be searched.</param>
/// <param name="globalId">Global ID to find duplicates for</param>
/// <param name="manufacturer">Manufacturer to find duplicates for</param>
/// <returns>True if a product exists with the specified
/// global ID and manufacturer and different ID, otherwise false.</returns>
bool DuplicateGlobalIdManufacturerExists(string id, string globalId, string manufacturer);
/// <summary>
/// Gets the product with the specified id.
/// </summary>
/// <param name="id">ID of product to retrieve</param>
/// <returns>New instance of Product with data for provided ID.</returns>
Product GetProduct(string id);
/// <summary>
/// Saves the provided Product as an existing record.
/// </summary>
/// <exception cref="System.InvalidOperationException">Thrown when no product is found for the ID
/// provided as product.Id.</exception>
/// <param name="product">Product to save</param>
void Save(Product product);
}
}
Putting the Pieces Together
Now that we have our object model, our database schema, and our operation contract, let’s actually implement the contract to interact with the database. Most of this code is pretty straight-forward as defined in the comments with the exception of the last four methods: CreateContract, CopyToContract, CreateEntity, and CopyToEntity. These methods deal with the conversions between the model class created by the Entity Framework and the data contract class. While it is possible to map POCO classes to entity models, it is not always best practice map your POCO classes exactly to your database. For example, in this implementation we use a ProductStatuses enum top populate the Product.Status property which is stored as a string. The translation to and from the enum values is performed by the CopyToContract and CopyToEntity methods. While this may seem cumbersome on the surface, it gives you very granular control over mapping the object model to the database.
//-----------------------------------------------------------------------
// <copyright file="ProductDataManager.cs" company="Streetlight Technologies L.L.C.">
// Copyright Streetlight Technologies L.L.C. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
namespace Streetlight.Store.DataAccess
{
using System;
using System.Collections.Generic;
using System.Linq;
using Streetlight.Store.Contracts;
/// <summary>
/// Provides an instance of IProductManager using the Entity Framework to manage Product data.
/// </summary>
public class ProductDataManager : IProductManager
{
/// <summary>
/// Instance of DataManager
/// </summary>
private DataManager _dataManager;
/// <summary>
/// Initializes a new instance of the <see cref="ProductDataManager"/> class.
/// </summary>
/// <param name="dataManager">DataManager instance containing this ProductDataManager instance.</param>
public ProductDataManager(DataManager dataManager)
{
_dataManager = dataManager;
}
/// <summary>
/// Returns a list of all products.
/// </summary>
/// <returns>New instance of List<Product> containing data for all products</returns>
public List<Product> ListAll()
{
return _dataManager
.Context
.Products
.ToList()
.Select(p => CreateContract(p))
.ToList();
}
/// <summary>
/// Stores the provided Product as a new record.
/// </summary>
/// <param name="product">Product to be saved</param>
public void CreateNew(Product product)
{
DataModels.Product productData = CreateEntity(product);
_dataManager.Context.Products.Add(productData);
_dataManager.Context.SaveChanges();
product.Id = Convert.ToString(productData.ProductId);
}
/// <summary>
/// Checks to see if a product exists with the specified item number and different
/// ID. This function can be used to ensure exclusivity of item numbers. Returns true
/// if a product exists with the specified item number and different ID, otherwise
/// returns false.
/// </summary>
/// <param name="id">ID of item to validate item number for. If parameter is null,
/// empty, or whitespace, all existing items will be searched.</param>
/// <param name="itemNumber">Item number to check for duplicates</param>
/// <returns>True if a product exists with the specified product number and different ID,
/// otherwise false.</returns>
public bool DuplicateItemNumberExists(string id, string itemNumber)
{
IQueryable<DataModels.Product> query = _dataManager.Context.Products.Where(p => p.ItemNumber == itemNumber);
if (!string.IsNullOrWhiteSpace(id))
{
int idValue = ConversionHelper.TryParsePositiveInt("id", id);
query = query.Where(p => p.ProductId != idValue);
}
return query.Any();
}
/// <summary>
/// Checks to see if a product exists with the specified global ID and manufacturer and
/// different ID. This function can be used to ensure exclusivity of Global ID/
/// Manufacturer combinations. Returns true if a product exists with the specified
/// global ID and manufacturer and different ID, otherwise returns false.
/// </summary>
/// <param name="id">ID of item to validate item number for. If parameter is null,
/// empty, or whitespace, all existing items will be searched.</param>
/// <param name="globalId">Global ID to find duplicates for</param>
/// <param name="manufacturer">Manufacturer to find duplicates for</param>
/// <returns>True if a product exists with the specified
/// global ID and manufacturer and different ID, otherwise false.</returns>
public bool DuplicateGlobalIdManufacturerExists(string id, string globalId, string manufacturer)
{
IQueryable<DataModels.Product> query = _dataManager.Context.Products.Where(p => p.GlobalId == globalId && p.Manufacturer == manufacturer);
if (!string.IsNullOrWhiteSpace(id))
{
int idValue = ConversionHelper.TryParsePositiveInt("id", id);
query = query.Where(p => p.ProductId != idValue);
}
return query.Any();
}
/// <summary>
/// Gets the product with the specified id.
/// </summary>
/// <param name="id">ID of product to retrieve</param>
/// <returns>New instance of Product with data for provided ID.</returns>
public Product GetProduct(string id)
{
int idValue;
if (!int.TryParse(id, out idValue))
{
throw new InvalidOperationException("Parameer \"id\" must evaluate to an integer.");
}
DataAccess.DataModels.Product productData = _dataManager.Context.Products.FirstOrDefault(p => p.ProductId == idValue);
if (productData == null)
{
return null;
}
else
{
return CreateContract(productData);
}
}
/// <summary>
/// Saves the provided Product as an existing record.
/// </summary>
/// <exception cref="System.InvalidOperationException">Thrown when no product is found for the ID
/// provided as product.Id.</exception>
/// <param name="product">Product to save</param>
public void Save(Product product)
{
if (product == null)
{
throw new ArgumentNullException("product");
}
int id;
if (!int.TryParse(product.Id, out id))
{
throw new InvalidOperationException("Parameer \"id\" must evaluate to an integer.");
}
DataModels.Product productData = _dataManager.Context.Products.FirstOrDefault(p => p.ProductId == id);
if (productData == null)
{
throw new InvalidOperationException(string.Format("Product not found for id {0}.", id));
}
CopyToEntity(product, productData);
_dataManager.Context.SaveChanges();
CopyToContract(productData, product);
}
/// <summary>
/// Creates a new instance of Contracts.Product and copies values from specified DataModels.Product instance.
/// </summary>
/// <param name="productData">DataModels.Product containing data</param>
/// <returns>New instance of Contracts.Product</returns>
private static Product CreateContract(DataAccess.DataModels.Product productData)
{
Product product = new Product();
CopyToContract(productData, product);
return product;
}
/// <summary>
/// Copies values from DataModels.Product to Contracts.Product.
/// </summary>
/// <param name="productData">DataModels.Product containing values to copy</param>
/// <param name="product">Contracts.Product to copy to</param>
private static void CopyToContract(DataAccess.DataModels.Product productData, Product product)
{
product.Id = Convert.ToString(productData.ProductId);
product.ItemNumber = productData.ItemNumber;
product.GlobalId = productData.GlobalId;
product.Name = productData.Name;
product.Manufacturer = productData.Manufacturer;
product.PartNumber = productData.PartNumber;
product.Description = productData.Description;
product.Price = productData.Price;
product.Length = productData.Length;
product.Width = productData.Width;
product.Height = productData.Height;
product.Weight = productData.Weight;
product.PackagingTypeCode = productData.PackagingTypeCode;
if (productData.StatusId.HasValue)
{
product.Status = Convert.ToString((ProductStatuses)productData.StatusId);
}
}
/// <summary>
/// Creates a new instance of DataModels.Product and copies values from specified Contracts.Product instance.
/// </summary>
/// <param name="product">Contracts.Product containing data</param>
/// <returns>New instance of DataModels.Product</returns>
private static DataAccess.DataModels.Product CreateEntity(Product product)
{
DataModels.Product productData = new DataModels.Product();
CopyToEntity(product, productData);
return productData;
}
/// <summary>
/// Copies values from Contracts.Product to DataModels.Product.
/// </summary>
/// <param name="product">Contracts.Product containing values to copy</param>
/// <param name="productData">DataModels.Product to copy to</param>
private static void CopyToEntity(Product product, DataAccess.DataModels.Product productData)
{
productData.ItemNumber = product.ItemNumber;
productData.GlobalId = product.GlobalId;
productData.Name = product.Name;
productData.Manufacturer = product.Manufacturer;
productData.PartNumber = product.PartNumber;
productData.Description = product.Description;
productData.Price = product.Price;
productData.Length = product.Length;
productData.Width = product.Width;
productData.Height = product.Height;
productData.Weight = product.Weight;
productData.PackagingTypeCode = product.PackagingTypeCode;
if (!string.IsNullOrEmpty(product.Status))
{
productData.StatusId = (byte)Enum.Parse(typeof(ProductStatuses), product.Status);
}
}
}
}