Doug.Instance

Improving the world one post at a time
posts - 22 , comments - 47 , trackbacks - 0

Sunday, March 10, 2013

Streetlight Store–Part IV: Relational Entities and Product Categories

In Part III, we started creating a very basic MVC application to host our store.  We also created a simple table to store products and methods to perform CRUD transactions and validate data.  Now we we introduce building product categories that can be related to products in a “many-to-many” relationship.  This means that multiple products can be assigned to a category and a product can be assigned to multiple categories. 

Creating the Tables

The category table is very simple and consists of a name and description.

/* ProductCategory.sql
* Copyright Streetlight Technologies L.L.C. All rights reserved.
*/
CREATE TABLE dbo.ProductCategory
(
    [ProductCategoryId] int identity(1, 1), -- Primary Key
    [Name] varchar(MAX), -- Prouct category name
    [Description] varchar(MAX), -- Product category description
    CONSTRAINT ProductCategory_PK PRIMARY KEY CLUSTERED
    (
        ProductCategoryId
    )
)
go

Next, we will create a table to relate categories to products.

/* ProductCategoryProduct.sql
* Copyright Streetlight Technologies L.L.C. All rights reserved.
*/
CREATE TABLE dbo.ProductCategoryProduct
(
    [ProductCategoryId] int not null, -- Primary Key
    [ProductId] int not null,
    CONSTRAINT ProductCategoryProduct_PK PRIMARY KEY CLUSTERED
    (
        ProductCategoryId,
        ProductId
    )
)
go

Then we create the foreign keys to link the three tables together.

ALTER TABLE dbo.ProductCategoryProduct
ADD CONSTRAINT ProductCategoryProduct_ProductCategory_FK FOREIGN KEY
(ProductCategoryId) REFERENCES ProductCategory(ProductCategoryId)
go

ALTER TABLE dbo.ProductCategoryProduct
ADD CONSTRAINT ProductCategoryProduct_Product_FK FOREIGN KEY
(ProductId) REFERENCES Product(ProductId)
go

After refreshing our entity model, it looks like this:

image

Note that the “ProductCategoryProduct” table does not appear in our model.  That is because there is no additional data other than foreign keys in the table so the Entity Framework considers the table trivial and handles it via the navigation properties in the Product and ProductCategory entities.  As we will see in the implementation of the database code, this isn’t ideal.  However, since it is the standard behavior for the Entity Framework and it should cause significant performance problems for a relatively low-traffic e-commerce site (we assume we are not hosting Amazon.com or eBay), we are going to work around this limitation.

Wiring Up the Data Access Layer

The first four methods to handle product categories are basically the same as the CRUD methods for products we saw in Part I.

/// <summary>
/// Returns a list of all categories.
/// </summary>
/// <returns>New instance of List&lt;ProductCategory&gt; containing data for all categories</returns>
public List<ProductCategory> ListAllCategories()
{
    return _dataManager
        .Context
        .ProductCategories
        .ToList()
        .Select(c => new ProductCategory
            {
                Id = Convert.ToString(c.ProductCategoryId),
                Name = c.Name,
                Description = c.Description
            })
        .ToList();
}

/// <summary>
/// Stores the category provided as a new record.
/// </summary>
/// <param name="category">Category to be saved</param>
public void CreateNewCategory(ProductCategory category)
{
    if (category == null)
    {
        throw new ArgumentNullException("category");
    }

    DataModels.ProductCategory categoryData = new DataModels.ProductCategory
    {
        Name = category.Name,
        Description = category.Description
    };

    _dataManager.Context.ProductCategories.Add(categoryData);
   
    _dataManager.Context.SaveChanges();

    category.Id = Convert.ToString(categoryData.ProductCategoryId);
}

/// <summary>
/// Gets the category with the specified ID.
/// </summary>
/// <param name="id">ID of category to return</param>
/// <returns>New instance of ProductCategory with data for provided ID.</returns>
public ProductCategory GetCategory(string id)
{
    int idValue = ConversionHelper.TryParsePositiveInt("id", id);

    DataModels.ProductCategory categoryData = _dataManager
        .Context
        .ProductCategories
        .FirstOrDefault(c => c.ProductCategoryId == idValue);

    if (categoryData == null)
    {
        return null;
    }

    return new ProductCategory { Id = id, Name = categoryData.Name, Description = categoryData.Description };
}

/// <summary>
/// Saves the provided category as an existing recod.
/// </summary>
/// <exception cref="System.InvalidOperationException">Thrown when no caetgory is found for the ID
/// provided as category.Id.</exception>
/// <param name="category">Category to save</param>
public void SaveCategory(ProductCategory category)
{
    if (category == null)
    {
        throw new ArgumentNullException("category");
    }

    int id = ConversionHelper.TryParsePositiveInt("category.Id", category.Id);

    DataModels.ProductCategory categoryData = _dataManager.Context.ProductCategories.FirstOrDefault(c => c.ProductCategoryId == id);

    if (category == null)
    {
        throw new InvalidOperationException(string.Format("Category not found for id {0}.", id));
    }

    categoryData.Name = category.Name;
    category.Description = category.Description;
   
    _dataManager.Context.SaveChanges();
}

Since we need to relate products to categories, we need to be able to find out what categories are already assigned to a product, assign categories, and remove categories.  The validation in this case is a little more simple.  We are going to follow the “law of least astonishment” and simply ignore an attempt to create a duplicate assignment or delete an assignment which does not exist.

/// <summary>
/// Lists categories associated with the specified ID.
/// </summary>
/// <param name="id">ID of product to list categories for</param>
/// <returns>New instance of List&lt;ProductCategory&gt; containing categories associated with product.</returns>
public List<ProductCategory> ListCategoriesForProduct(string id)
{
    int idValue = ConversionHelper.TryParsePositiveInt("id", id);

    DataModels.Product productData = _dataManager
        .Context
        .Products
        .Include("ProductCategories")
        .FirstOrDefault(p => p.ProductId == idValue);

    if (productData == null)
    {
        throw new InvalidOperationException(string.Format("Product not found for id {0}.", idValue));
    }

    return productData
        .ProductCategories
        .Select(c => new ProductCategory { Id = Convert.ToString(c.ProductCategoryId), Name = c.Name, Description = c.Description })
        .ToList();
}

/// <summary>
/// Adds the product with the specified ID to the category with the specified ID.  If a relationship
/// already exists, nothing is done.
/// </summary>
/// <param name="productId">ID of product to add</param>
/// <param name="categoryId">ID of category to add to</param>
public void AddProductToCategory(string productId, string categoryId)
{
    int productIdValue = ConversionHelper.TryParsePositiveInt("productId", productId);

    int categoryIdValue = ConversionHelper.TryParsePositiveInt("categoryId", categoryId);

    DataModels.ProductCategory category = _dataManager
        .Context
        .ProductCategories
        .Include("Products")
        .FirstOrDefault(c => c.ProductCategoryId == categoryIdValue);

    if (category == null)
    {
        throw new InvalidOperationException(string.Format("Category not found for id {0}.", categoryIdValue));
    }

    if (category.Products.Any(p => p.ProductId == productIdValue))
    {
        return;
    }

    DataModels.Product product = _dataManager
        .Context
        .Products
        .FirstOrDefault(p => p.ProductId == productIdValue);

    if (product == null)
    {
        throw new InvalidOperationException(string.Format("Product not found for id {0}.", productIdValue));
    }

    category.Products.Add(product);

    _dataManager.Context.SaveChanges();
}

/// <summary>
/// Removes the product with the specified ID from the category with the specified ID.  If no relationship
/// exists, nothing is done.
/// </summary>
/// <param name="productId">ID of product to remove</param>
/// <param name="categoryId">ID of category to remove from</param>
public void RemoveProductFromCategory(string productId, string categoryId)
{
    int productIdValue = ConversionHelper.TryParsePositiveInt("productId", productId);

    int categoryIdValue = ConversionHelper.TryParsePositiveInt("categoryId", categoryId);

    DataModels.ProductCategory category = _dataManager
        .Context
        .ProductCategories
        .Include("Products")
        .FirstOrDefault(c => c.ProductCategoryId == categoryIdValue);

    if (category == null)
    {
        throw new InvalidOperationException(string.Format("Category not found for id {0}.", categoryIdValue));
    }

    if (!category.Products.Any(p => p.ProductId == productIdValue))
    {
        return;
    }

    DataModels.Product product = _dataManager
        .Context
        .Products
        .FirstOrDefault(p => p.ProductId == productIdValue);

    if (product == null)
    {
        throw new InvalidOperationException(string.Format("Product not found for id {0}.", productIdValue));
    }

    category.Products.Remove(product);

    _dataManager.Context.SaveChanges();
}

 

Next, we create the ProductCategoriesController to do the CRUD transactions.

public class ProductCategoriesController : StoreController
{
    public ActionResult Index()
    {
        List<ProductCategory> model = TransactionManager.Products.ListAllCategories();

        return View("List", model);
    }

    public ActionResult List()
    {
        List<ProductCategory> model = TransactionManager.Products.ListAllCategories();

        return View(model);
    }

    public ActionResult Create()
    {
        ProductCategory model = new ProductCategory();

        return View(model);
    }

    [HttpPost]
    public ActionResult Create(ProductCategory model)
    {
        if (ModelState.IsValid)
        {
            TransactionManager.Products.CreateNewCategory(model);

            return RedirectToAction("Edit", new { id = model.Id });
        }

        return View(model);
    }

    public ActionResult Edit(string id)
    {
        ProductCategory model = TransactionManager.Products.GetCategory(id);

        return View(model);
    }

    [HttpPost]
    public ActionResult Edit(ProductCategory model)
    {
        if (ModelState.IsValid)
        {
            TransactionManager.Products.SaveCategory(model);
        }

        return View(model);
    }
}

Then, we add methods to the ProductsController to list categories and add and remove categories.

public ActionResult CategoryList(string id)
{
    ViewBag.Id = id;

    List<ProductCategory> model = TransactionManager.Products.ListCategoriesForProduct(id);

    return PartialView(model);
}

public ActionResult AddCategory(string id)
{
    ViewBag.Id = id;

    List<ProductCategory> model = TransactionManager.Products.ListAllCategories();

    return PartialView(model);
}

[HttpPost]
public ActionResult AddCategory(string id, string categoryId)
{
    TransactionManager.Products.AddProductToCategory(id, categoryId);

    return RedirectToAction("Edit", new { id = id });
}

public ActionResult RemoveCategory(string id, string categoryId)
{
    TransactionManager.Products.RemoveProductFromCategory(id, categoryId);

    return RedirectToAction("Edit", new { id = id });
}

The CategoryList action displays a partial view with a list of categories associated with a product and links to remove the category. The AddCategory action displays a partial view with a drop-down list to select a category and a button to add it to the product.  The resulting product view looks like this:

image

Download source code from CodePlex.

Posted On Sunday, March 10, 2013 2:41 PM | Comments (0) |

Wednesday, March 6, 2013

Streetlight Store - Part III: Implementation in an MVC 4 Application

To keep things simple, I am going to retain the default security framework created by Visual Studio when I created the application.  However, I am going to change the database name to something more meaningful.  So in web.config, I am changing the connection string to this:

<add name="DefaultConnection" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=StreetlightStoreSample;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\StreetlightStoreSample.mdf" providerName="System.Data.SqlClient" />

Next we’ll add the connection string from our database project found in Streetlight.Store.DataAccess\app.config:

<add name="StoreEntities" connectionString="metadata=res://*/DataModels.StoreDataModel.csdl|res://*/DataModels.StoreDataModel.ssdl|res://*/DataModels.StoreDataModel.msl;provider=System.Data.SqlClient;provider connection string=&quot;data source=.\SQLEXPRESS;initial catalog=SAMPLE_STORE;integrated security=True;MultipleActiveResultSets=True;App=EntityFramework&quot;" providerName="System.Data.EntityClient" />

Now let’s look at the home page which is the Views\Home\Index.cshtml view.  Most of the changes are cosmetic until we get to the lists at the bottom of the page.  Here we will link to some actions to be added to the MVC application:

<ol class="round">
    <li class="one">
        <h5>Store Management</h5>
        Manage your store by creating products and handling customer orders.
        @Html.ActionLink("Manage store…", "Index", "Admin")
    </li>

    <li class="two">
        <h5>Browse Products</h5>
        Customers hopefully want to buy your products so let's
        @Html.ActionLink("browse products…", "Index", "Products")
    </li>

    <li class="three">
        <h5>User Customization</h5>
        @if (Request.IsAuthenticated)
        {
            <text>Hey! You registered!  Now you can look at your past orders and
            manage your account information.</text>
        }
        else
        {
            <text>@Html.ActionLink("Register", "Register", "Account", routeValues: null, htmlAttributes: new { id = "registerLink" }) or
            @Html.ActionLink("Log in", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" })
            to view past orders and manage your account information.</text>
        }
    </li>
</ol>

 

Next we will change the header menu found in \Views\Shared\_Layout.cshtml:

<nav>
    <ul id="menu">
        <li>@Html.ActionLink("Home", "Index", "Home")</li>
        <li>@Html.ActionLink("Admin", "Index", "Admin")</li>
        <li>@Html.ActionLink("Shop", "Index", "Store")</li>
    </ul>
</nav>

 

As a result, our new home page looks like this:

image

Note that this also required a change to Controllers\HomeController.cs controller class to change the message displayed by the Index action.

The Products Controller

Let’s create a ProductsController controller class to call the methods we created in Part II to manage products.  The first code we need to add to this class is the code to handle creation and disposal of the transaction manager:

public ITransactionManager TransactionManager { get; private set; }

public ProductsController()
{
    TransactionManager = new Streetlight.Store.DataAccess.DataManager();
}

protected override void Dispose(bool disposing)
{
    if (disposing && TransactionManager != null)
    {
        TransactionManager.Dispose();
    }

    base.Dispose(disposing);
}

 

Note that in this case we create a strongly-typed instance of the specific implementation of ITransactionManager (Streetlight.Store.DataAccess.DataManager) introduced in Part II.  In future posts, we will look at further abstracting the initialization of the TransactionManager property.

Next we will create the get and post actions to create a new product:

public ActionResult Create()
{
    Product model = new Product();

    return View(model);
}

[HttpPost]
public ActionResult Create(Product model)
{
    if (TransactionManager.Products.DuplicateItemNumberExists(string.Empty, model.ItemNumber))
    {
        ModelState.AddModelError("ItemNumber", "A product already exists with this item number.");
    }

    if (TransactionManager.Products.DuplicateGlobalIdManufacturerExists(string.Empty, model.GlobalId, model.Manufacturer))
    {
        ModelState.AddModelError("GlobalId", "A product already exists with this Global ID/Manufacturer combination.");
    }

    if (ModelState.IsValid)
    {
        TransactionManager.Products.CreateNew(model);

        return RedirectToAction("Edit", new { id = model.Id });
    }
    else
    {
        return View(model);
    }
}

 

To create the view to create a new product, we will use the default scaffolding in Visual Studio:

image

For now we will remove the status field and change the title so the final view looks like this in Views\Products\Create.cshtml:

@model Streetlight.Store.Contracts.Product

@{
    ViewBag.Title = "New Product";
}

<h2>New Product</h2>

@using (Html.BeginForm()) {
    @Html.ValidationSummary(true)

    <fieldset>
        <legend>Product</legend>

        <div class="editor-label">
            @Html.LabelFor(model => model.Name)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Name)
            @Html.ValidationMessageFor(model => model.Name)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.Price)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Price)
            @Html.ValidationMessageFor(model => model.Price)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.PackagingTypeCode)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.PackagingTypeCode)
            @Html.ValidationMessageFor(model => model.PackagingTypeCode)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.GlobalId)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.GlobalId)
            @Html.ValidationMessageFor(model => model.GlobalId)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.Manufacturer)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Manufacturer)
            @Html.ValidationMessageFor(model => model.Manufacturer)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.PartNumber)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.PartNumber)
            @Html.ValidationMessageFor(model => model.PartNumber)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.Weight)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Weight)
            @Html.ValidationMessageFor(model => model.Weight)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.Description)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Description)
            @Html.ValidationMessageFor(model => model.Description)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.ItemNumber)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.ItemNumber)
            @Html.ValidationMessageFor(model => model.ItemNumber)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.Length)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Length)
            @Html.ValidationMessageFor(model => model.Length)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.Width)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Width)
            @Html.ValidationMessageFor(model => model.Width)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.Height)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Height)
            @Html.ValidationMessageFor(model => model.Height)
        </div>

        <p>
            <input type="submit" value="Create" />
        </p>
    </fieldset>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

 

We can then create the List and Edit actions and view using the default scaffolding as well with more minimal changes.

Download the full source code from CodePlex.

Posted On Wednesday, March 6, 2013 6:42 PM | Comments (2) |

Tuesday, March 5, 2013

Streetlight Store - Part II: Black Boxing Products and Improved Architecture

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);
            }
        }
    }
}


 

Download the source code from CodePlex.

Posted On Tuesday, March 5, 2013 9:49 PM | Comments (0) |

Wednesday, December 26, 2012

Simple MVC Imperative Validation

After looking at FluentValidation, I decided that most of what I needed was a way to do server-side validation of more complicated rules.  While this is something FluentValidation does, it seemed like more than I needed so I set out to create a simple extension method to do what I needed.  While it is still not necessarily perfect, it is simple and works.

   1:          public static void Validate<T, TResult>(this Controller controller, T model, Expression<Func<T, TResult>> expression, Func<TResult, bool> predicate, string message)
   2:          {
   3:              MemberExpression memberExpression = expression.Body as MemberExpression;
   4:   
   5:              if (memberExpression == null)
   6:              {
   7:                  throw new ArgumentException("expression must be a property.");
   8:              }
   9:   
  10:              PropertyInfo propertyInfo = memberExpression.Member as PropertyInfo;
  11:   
  12:              if (propertyInfo == null)
  13:              {
  14:                  throw new ArgumentException("expression must be a property.");
  15:              }
  16:   
  17:              TResult result = (TResult)propertyInfo.GetValue(model, new object[0]);
  18:   
  19:              if (!predicate(result))
  20:              {
  21:                  string key = propertyInfo.Name;
  22:               
  23:                  controller.ModelState.AddModelError(key, message);
  24:              }
  25:          }

Here is a sample implementation.  Obviously this is a trivial case but any condition can be used and will add the error if the expression evaluates to false.

 

   1:              this.Validate(
   2:                  model, // Model
   3:                  m => m.Name, // Parameter to use for key
   4:                  n => !string.IsNullOrEmpty(n), // Expression to add error if false
   5:                  "Name cannot be blank."); // Message to add if expression returns false

Posted On Wednesday, December 26, 2012 3:59 PM | Comments (0) |

Friday, April 20, 2012

Two Best SharePoint Utility Methods Ever Written

SharePoint is very handy as a repository for data.  There are things (particularly in the intranet world) which really don't need a dedicated database or application and SharePoint is a great enabler.  However, if you want to build a lightweight, standards-friendly web page using SharePoint, you have a challenge ahead of you.

Here is the problem I was trying to solve:  I have a SharePoint site that serves as the home page for an organization.  I needed to be able to pull in various content from other sites (on other servers) and present it in a nice, clean layout.  In order to keep the maintenance of this page as simple as possible, I wanted to use JavaScript/jQuery to fetch data and then use as much standard HTML/CSS for the page itself as possible.

Luckily, SharePoint has several client APIs.  There are some fairly easy ways you can get data from SharePoint lists (the fundamental data structure used by SharePoint).  I was able to find a workable solution rather quickly to pull list data in using the ASP.Net web services via jQuery.  This worked just fine until I found some content on a different server and ran into the cross-site scripting limitations of jQuery ajax.

In order to continue, I knew I would have to use JSONP.  The easiest way I could think of to do this would be to write some code to act as an intermediary between the JavaScript client and the SharePoint servers.  This intermediary utility was broken up into two methods to perform these 2 functions:

  1. Retrieve a SharePoint list as an ADO.Net DataSet.
  2. Retrieve a SharePoint list as JSONP.

The first step was to create an ASP.Net MVC project and add references to Microsoft.SharePoint.Client and Microsoft.SharePoint.Client.Runtime. 

Next I created the method to get a DataSet.  I understand DataSets aren't the "best" vehicle for transferring data, but they are ubiquitous so I felt this was the best short-term method to ensure potential reuse.  That method looks like this:

   1:          public static DataSet GetListAsDataSet(string serverUrl, string listName, string query)
   2:          {
   3:              ClientContext context = new ClientContext(serverUrl);
   4:   
   5:              List list = context.Web.Lists.GetByTitle(listName);
   6:   
   7:              if (list != null)
   8:              {
   9:                  context.Load(list);
  10:   
  11:                  FieldCollection fields = list.Fields;
  12:   
  13:                  context.Load(fields);
  14:                  context.ExecuteQuery();
  15:   
  16:                  DataSet ds = new DataSet();
  17:                  DataTable table = new DataTable();
  18:                  ds.Tables.Add(table);
  19:   
  20:                  foreach (Field field in fields)
  21:                  {
  22:                      if (!table.Columns.Cast<DataColumn>().Any(c => c.ColumnName == field.Title))
  23:                      {
  24:                          table.Columns.Add(field.Title, typeof(object));
  25:                          string temp = field.InternalName;
  26:                      }
  27:                  }
  28:   
  29:                  CamlQuery camlQuery = new CamlQuery();
  30:   
  31:                  if (!string.IsNullOrEmpty(query))
  32:                  {
  33:                      camlQuery.ViewXml = query;
  34:                  }
  35:   
  36:                  ListItemCollection items = list.GetItems(camlQuery);
  37:   
  38:                  context.Load(items);
  39:                  context.ExecuteQuery();
  40:   
  41:                  foreach (ListItem item in items)
  42:                  {
  43:                      DataRow row = table.NewRow();
  44:                      table.Rows.Add(row);
  45:   
  46:                      foreach (Field field in fields)
  47:                      {
  48:                          string key = field.Title;
  49:   
  50:                          if (item.FieldValues.ContainsKey(field.InternalName))
  51:                          {
  52:                              if (item[field.InternalName] == null)
  53:                              {
  54:                                  row[key] = DBNull.Value;
  55:                              }
  56:                              else
  57:                              {
  58:                                  row[key] = item[field.InternalName];
  59:                              }
  60:                          }
  61:                      }
  62:                  }
  63:   
  64:                  return ds;
  65:              }
  66:   
  67:              return null;
  68:          }

I then created an action method in my MVC controller to get the DataSet created by the list and convert it to an object with 2 properties.  The "fields" property contains an array of field names.  The "rowData" property contains an array of arrays representing rows and columns of data.  That method looks like this:

   1:          public ActionResult GetListAsJsonp(string server, string listName, int? rowLimit, string callback)
   2:          {
   3:              string query = null;
   4:   
   5:              if (rowLimit.HasValue)
   6:              {
   7:                  query = string.Format("<View><RowLimit>{0}</RowLimit></View>", rowLimit);
   8:              }
   9:   
  10:              DataSet ds = Quality.Common.SharePoint.ListHelper.GetListAsDataSet(server, listName, query);
  11:   
  12:              if (ds == null || ds.Tables.Count == 0)
  13:              {
  14:                  return new EmptyResult();
  15:              }
  16:   
  17:              DataTable table = ds.Tables[0];
  18:   
  19:              JavaScriptSerializer serializer = new JavaScriptSerializer();
  20:   
  21:              List<object> rowData = new List<object>();
  22:   
  23:              foreach (DataRow row in table.Rows)
  24:              {
  25:                  rowData.Add(row.ItemArray);
  26:              }
  27:   
  28:              var data = new { fields = table.Columns.Cast<DataColumn>().Select(c => c.ColumnName).ToArray(), rowData = rowData.ToArray() };
  29:   
  30:              return Content(string.Format("{0}({1});", callback, serializer.Serialize(data)), "application/json");
  31:          }

You can then use this MVC action like this:

   1:  function getList() {
   2:      $("#test").empty();
   3:      $("#test").append("Fetching data...<br/>");
   4:      $.ajax({
   5:          url: "http://myserver/Home/GetListAsJsonp/?server=" + $("#siteUrl").val() + "&listName=" + $("#listName").val() + "&rowLimit=5",
   6:          type: 'GET',
   7:          dataType: "jsonp",
   8:          success: testResult,
   9:          error: function (err, msg) { alert(msg); }
  10:      });
  11:  }
  12:   
  13:  function testResult(data) {
  14:      $("#test").append("Got data.<br/>");
  15:   
  16:      var output = "<table>";
  17:      
  18:      output = output + "<tr>"
  19:   
  20:      for (i in data.fields) {
  21:          output = output + "<th>";
  22:          output = output + data.fields[i];
  23:          output = output + "</th>";
  24:      }
  25:   
  26:      for (index in data.rowData) {
  27:          
  28:          output = output + "<tr>";
  29:          
  30:          for (i in data.fields) {
  31:              output = output + "<td>";
  32:              output = output + data.rowData[index][i];
  33:              output = output + "</td>";
  34:          }
  35:   
  36:          output = output + "</tr>";
  37:      }
  38:   
  39:      output = output + "</table>";
  40:   
  41:      $("#test").append(output);
  42:  }

Now when I say these are the best methods ever written, that doesn't mean this is the best code. It's just very useful in my opinion.  Hope you agree.

Posted On Friday, April 20, 2012 10:15 AM | Comments (1) |

Thursday, February 16, 2012

The Enemy of My Friend Writes Bad MVC Controller Actions (Microsoft)

Recommendation: Don't use the same action names for get and (Ajax) post.

I am always amazed at how bad Microsoft's code examples are.  Code generated using the default templates in Visual Studio is not much better.  To find out, create an empty project (pick your favorite type) and run static code analysis or FxCop and see how many warnings you see.  For some real fun try running StyleCop.  The default templates (and therefore commonly-used standard practices) are also not very good in my opinion.  Consider the following controller code generated using the standard MVC 3 template:

        //
        // GET: /Test/Create

        public ActionResult Create()
        {
            return View();
        } 

        //
        // POST: /Test/Create

        [HttpPost]
        public ActionResult Create(FormCollection collection)
        {
            try
            {
                // TODO: Add insert logic here

                return RedirectToAction("Index");
            }
            catch
            {
                return View();
            }
        }

For those of you who have used MVC for a while, this looks pretty standard.  "So what's the big deal?" you ask. Here is my beef: there is no value to having two controller actions named "Create".  My personal opinion, is that this convention was used to smooth the transition from web forms.  The first Create method would be similar to a Page_Load event handler and the second one would be similar to a Button_Click event handler.  To use more web form language, the first method handles the initial request and the second one handles the "postback".  For people used to dealing with web forms, it makes it more obvious that these two methods are dealing with the same "page".  The only real advantage is that you don't have to specify the action name in Html.BeginForm.  This can be a good thing because it eliminates a magic string from your view.

So let's examine this more closely and see where we start running into problems.  For the sake of argument, let's say that this is a search page.  Therefore you want to use a GET transaction instead of POST so the user can see the URL, bookmark it to re-run their search, etc.  You remove the [HttpPost] attribute. You will now get an error that the action is ambiguous between action methods.

The solution to this problem is simple, just rename one of the methods. My recommendation is to call the first method "New". This then maps to the nice route "/{controller}/New".  In general this is something users expect to see in a URL - what they are looking at so I like to use nouns or adjectives for these actions (Details, New, Info, Order, Product, etc.).  The form would then call the "Create" action.  In this case I like to make my actions that do something use verbs (Create, Update, Delete, List, Rename, etc.).

This convention really starts to make even more sense once you start having multiple Ajax calls from a single page.  After all, why use a paradigm based on full-page posting (Html.BeginForm in MVC) when most applications will use partial-page posts or other script-based methods?

Posted On Thursday, February 16, 2012 11:29 PM | Comments (6) |

Tuesday, February 7, 2012

Problem: DropDownList Always Posts Blank Value in ASP.Net MVC 2

Consider the following code:

<%= Html.DropDownList("State", new SelectList(new string[] { "","AK","AL","AR","AS","AZ","CA","CO","CT","DC","DE","FL","GA","GU","HI","IA","ID",
                    "IL","IN","KS","KY","LA","MA","MD","ME","MH","MI","MN","MO","MS","MT","NC","ND","NE","NH","NJ","NM","NV","NY",
                    "OH","OK","OR","PA","PR","PW","RI","SC","SD","TN","TX","UT","VA","VI","VT","WA","WI","WV","WY"})) %>

Simple enough, right?  Display a dropdown list with a value for each state.  There is only one problem.  The resulting HTML looks like this:

<select id="State" name="state"><option></option><option>AK</option>...

While this is valid HTML in most browsers, it fails in IE 8 running in compatability mode.  In essence, it is treated like this:

<option value=""></option><option value="">AK</option>...

A simple solution is to force a value like this:

<%= Html.DropDownList("State", new SelectList((new string[] { "","AK","AL","AR","AS","AZ","CA","CO","CT","DC","DE","FL","GA","GU","HI","IA","ID",
                    "IL","IN","KS","KY","LA","MA","MD","ME","MH","MI","MN","MO","MS","MT","NC","ND","NE","NH","NJ","NM","NV","NY",
                    "OH","OK","OR","PA","PR","PW","RI","SC","SD","TN","TX","UT","VA","VI","VT","WA","WI","WV","WY"}).Select(x => new { Text = x, Value = x}), "Value", "Text", null)) %>

You're welcome!

Posted On Tuesday, February 7, 2012 10:08 AM | Comments (0) |

Saturday, December 31, 2011

Streetlight Store - Part I: Introducton and Architecture

What is the Streetlight Store?

The Streetlight Store is a .Net library which uses the Entity Framework and a Microsoft SQL Server database to perform the "back-end" operations required for a typical e-commerce application.  While the Streetlight Store is intended to be used for e-commerce, it is "front-end agnostic" meaning that you could just as easily create a point-of-sale user interface.

Why is there a Streetlight Store?

Why develop another e-commerce solution when there are so many already available? For me the answer comes down to two simple reasons: control and continuity. By "control", I mean of course that I ultimately have control over the source code. Even though this is going to be published as an open-source project, the license and copyright will still be maintained by my company - Streetlight Technologies L.L.C. This allows me to incorporate features to meet my customers' needs while controlling the quality and security of the system. By "continuity", I mean that my customers can have faith that in the event I am unable to continue to support them for any reason, they can continue to receive support from just about any competent developer who can download my source code.

Why is there this Detailed Walk-Through?

I have found that the best way to find problems with your code is to simply try to explain it to someone else.  Over time, I have stopped asking peers to "look at my code" and instead now just say, "You can ignore me.  I am going to talk through my code until I find the problem so you can just go about your business."

How Does it Work?

The Streetlight Store uses a simple object model to send data back and forth to the database.  These objects can be found in the Streetlight.Store.Contracts namespace.  The formatting below is to give you an indicaton of the logical structure and does not necessarily represent the actual object model structure.

  • Product
    • ProductCategory
    • ProductPrice
    • ProductImage
      • ImageFile
    • ProductInventory
  • UserAccount
    • Customer
      • CustomerAddress
      • CustomerPhone
  • ShoppingCart
  • Order
    • OrderItem
  • Shipment

There are other miscellaneous objects, but these represent the "core" of the system.  These classes are NOT Entity Framework model classes.  They are simple, light-weight, POCO ("Plain Old CLR Object") objects.  This is because this system was designed first and foremost for use with web applications.  Web apps are (in spite of some peoples efforts to force them to act otherwise) stateless.  Therefore exposing the Entity Framework objects and their built-in state management can cause some pretty major problems if you don't execute things carefully.

An added benefit of using this "Data Contract" model is that this store can easily be implemented using web services.  In fact, the data contract classes are decorated with the attributes required for WCF services.

These contract objects are stored in and retreived from the database using "Data Manager" classes in the Streetlight.Store.DataAccess namespace:

  • DataManager
    • ProductDataManager
    • AccountDataManager
    • OrderDataManager
    • ShipmentDataManager
The main DataManager class contains instances of the other data managers which then contain references back to the main DataManager class such that any data manager can easily access any other data manager.  Creation and disposal of Entity Framework context instances is controlled by the main DataManager class.
 
There are some things the Streetlight Store does NOT do by design:
  • Cryptography (encryption/decryption)
  • Credit card processing
  • Shipping calculation

Cryptography is not included for security purposes.  I don't want ANYONE to know the techniques I use to secure sensitive information so publishing them as open-source wouldn't really make much sense.  Credit card processing companies often reqiure non-dislosure agreements for use of their APIs.  This of course prohibits the publishing of a specific implementation as open-source.  Shipping estimation is a very complicated process including many variables: selection of shipping carrier, physical characteristics of products (weight and size), selection of packaging, marketing and promotion (i.e. free shipping), etc.

The following interfaces are provided in the Streetlight.Store namespace to allow developers to implement these functions:

  • ICreditCardProcessor
  • IShippingCalculator
  • ICryptographyProvider
In the following articles, I will walk through the full design including a simple implementation using ASP.Net MVC.

Posted On Saturday, December 31, 2011 9:15 PM | Comments (0) |

Thursday, December 22, 2011

Practice Version Control Now...Thank Me Later

To me the answer of whether or not you need version control is simple - do you have users?  If the answer is yes, then you need version control.  Note that "version control" is not the same as "source control".  Source control refers to maintaining history of your source code.  Version control (also known as "configuration control" or "configuration management") is more than that.  Version control, as its name indicates, means tracking versions of your software.

The best way to illustrate the purpose of version control is through a simple example:

You develop an application.  While developing the application you go through several iterations throwing away large chunks of code, refactoring, redesigning, etc.  Finally you are happy with the application and you publish it.  Just to make this example more tangible, let's say it is a web application so now you have deployed it using your favorite hosting provider.  One way or another, you get some users who regularly visit your web site.

Your users like your application, but you know it could be better.  You start a massive redesign and start to totally refactor everything.  Next thing you know, you get a message from your web hosting provider saying they are implementing new security measures and if you are doing XYZ, you will need to change to ABC by the end of the month.  The good news is this change just requires a few lines of code and the even better news is you can include this in your new version with your massive redesign.

It is now the last day of the month.  Life has happened (as it tends to) and your new version is not ready.  You now have less than 24 hours to complete a massive redesign, test, migrate data, and deploy.  Now you realize a simple truth:

THIS WOULD BE SO MUCH EASIER IF I HAD MY ORIGINAL PRODUCTION SOURCE CODE!

Now this is a very simplistic and probably somewhat unlikely situation.  After all, you probably are at least zipping up or otherwise backing up copies of your code.  There is a good chance you are doing this when you release a new version.  If you are doing that, then you are doing some version control.  However, if you don't have a process you follow for EVERY VERSION then you run the risk of running into the problem I described above.

So here are some simple rules to follow to make sure you have everything you need to properly maintain production code while continuing to develop new and exciting features.

Rule #1: Create a "Main" Branch

The idea of "branching" or "forking" may be completely foreign to you and if not, you still might not think about it if you aren't using source control.  Branching means that you make copies of your code along logical paths.  To have logical paths you need to have a begining and that beginning is your main branch.  In this case, I am referring to a physical folder typically called "Main".

The most important aspect of your main branch is that it contains EVERYTHING required for the application.  That means common code, 3rd party code, 3rd party libraries, documentation, media, etc.  This ensures that you always have the correct version of all of those external resources for that specific project.  That may even mean using local copies of assemblies instead of the GAC to ensure you can control which version you are using.  You don't want an application to sit on the shelf for a year and break because you upgrade a component that's registered in the GAC.

For me, a typical main branch folder contains the following subfolders:

  • Bin (Folder for binary references typically from 3rd parties)
  • [Project].Web (Web application source code)
  • [Project].DataAccess (Data access library source code)
  • [Me].Utilities (My common code)

Rule #2: Create a Release Branch

Copy EVERYTHING from your main branch folder to a new release branch.  Typically this would be a subfolder underneath a "Releases" folder (i.e. "Releases\1.0").

Rule #3: Don't Touch the Release Branch

No matter how trivial you think a change might be, don't make it in your release branch.  This is perhaps the most important aspect of this process - retention of your released code.  Even bug fixes which look simple on the surface can lead down a rabbit hole of problems which could cause you to end up with mangled and inoperable code - exactly what we are trying to avoid.

If you have one or two bugs to fix, branch from your release branch to another release branch (i.e. "Releases\1.0.1") and make your fixes there.  If you have ongoing development (long-term work on new features or enhancements), create a "Development" branch (i.e. "Development" folder).

Rule #4: Merge Often and CAREFULLY

The reason we call this "Branching" and not simply "Copying" is because it implies that there is a "split" in the development process.  Consider your development can go down two paths - one to maintain what is in production and one to make long-term enhancements.  Merging allows those two paths to converge into a single set of source code.

Merging simply means taking the changes from the two different branches and combining them.  You can do this manually or by using a "diff" or "merge" tool.  Manual merging means that if you make a bug fix in a new release branch, you would then make the same change in your main and development branches.  This is perhaps the most thorough method because it ensures that you make each change incrementally and indivdually.  It isn't necessarily the most efficient method and there are many tools available to allow you to merge changes to multiple files relatively easily.

So the obvious question is where do the changes actually get combined?  As I mentioned earlier, the main branch is the beginning of your logical branching path.  In fact some people refer to this as your "trunk".  All merging should happen back through your main branch.

Merge CAREFLULLY however. Your main branch should be your "Gold" copy of your application.  Only merge back to main once you are sure that your changes are complete and fully tested.  After you merge, make sure that you didn't introduce any new bugs simply through the mechanics of merging.  For example, your "most recent" version of your code may be from a recent bug fix.  However, other changes in your development branch have made the bug fix obsolete.  If you overwrite the "older" file with the "new" bug fix, you could introduce new bugs.

Summary

The basic process looks something like this:

  1. Develop your application in your "Main" folder containing EVERYTHING you need for your application.
  2. Once you are ready to release your first version, create a copy of your or "Branch" in a "Release" subfolder under a "Releases" folder (i.e. "Releases\1.0").
  3. Use your release branch to deploy.
  4. Create a "Development" branch folder as a copy of your main branch which should now match your latest release.
  5. Do your ongoing (long-term) development in the development branch folder.
  6. For bug fixes, create a new release branch from your previous release branch and make your fixes there.
  7. "Merge" any changes from your release and development branches back to your main branch.  This may require a "diff" tool.  Merge from release to development via main.  Merge back to main ONLY once everything has been tested and is ready for release.
  8. Copy your main branch to a new release branch.
  9. Repeat from step 3.

Your file structure would look something like this:

  • [Project]
    • Main
      • Bin
      • [Project].Web
      • [Project].DataAccess
      • [Me].Utilities
    • Releases
      • 1.0
        • Bin
        • ...
      • 1.01
      • 1.02
      • 2.0
    • Development
      • Bin
      • ...

References

Microsoft has some very good guidance on source control which is specifically related to Team Foundation Server, but hidden between the lines of this guidance is good version control guidance and a lot of the foundation for what I describe above.

This Stack Overflow post has a good collection of diff tools by OS and free vs. pay.

Before You Post That Comment

Note that this process is about "Version Control" NOT "Source Control".  How you would branch and merge would be somewhat different if you are using a source control system to keep track of your source code history.  The process outlined above is specifically intended to be used when you are NOT using source control.

Posted On Thursday, December 22, 2011 10:48 PM | Comments (0) |

Tuesday, November 29, 2011

Custom Model Binding of IEnumerable Properties in ASP.Net MVC 2

MVC 2 provides a GREAT feature for dealing with enumerable types.  Let's say you have an object with a parent/child relationship and you want to allow users to modify multiple children at the same time.  You can simply use the following syntax for any indexed enumerables (arrays, generic lists, etc.) and then your values will bind to your enumerable model properties.

						1:  
						<% 
						using (Html.BeginForm("TestModelParameter", "Home"))
2: { %>
3: <table>4: <tr><th>ID</th><th>Name</th><th>Description</th></tr>
5: <%for (int i = 0; i < Model.Items.Count; i++)
6: { %>
7: <tr>
8:<td>
9: <%= i %>
10: </td>
11: <td>
12: <%= Html.TextBoxFor(m => m.Items[i].Name) %>
13: </td>
14: <td>
15: <%= Model.Items[i].Description %>
16: </td>
17: </tr>
18: <% } %>
19: </table>20: <input type="submit"/>21: <% } %>

Then just update your model either by passing it into your action method as a parameter or explicitly with UpdateModel/TryUpdateModel.

						1:  
						public ActionResult TestTryUpdate()
2: {
3: ContainerModel model = new ContainerModel();
4: TryUpdateModel(model);
5:
6: return View("Test", model);
7: }
8:
9: public ActionResult TestModelParameter(ContainerModel model)
10: {
11: return View("Test", model);
12: }

Simple right?  Well, not quite.  The problem is the DefaultModelBinder and how it sets properties.  In this case our model has a property that is a generic list (Items).  The first bad thing the model binder does is create a new instance of the list.  This can be fixed by making the property truly read-only by removing the set accessor.  However this won't help because this behaviour continues.  As the model binder iterates through the items to "set" their values, it creates new instances of them as well.  This means you lose any information not passed via the UI to your controller so in the examplel above the "Description" property would be blank for each item after the form posts.

One solution for this is custom model binding.  I have put together a solution which allows you to retain the structure of your model.  Model binding is a somewhat advanced concept so you may need to do some additional research to really understand what is going on here, but the code is fairly simple.  First we will create a binder for the parent object which will retain the state of the parent as well as some information on which children have already been bound.

						1:  
						public
						class ContainerModelBinder : DefaultModelBinder
2: {
3: /// <summary>
4: /// Gets an instance of the model to be used to bind child objects.
5: /// </summary>
6: public ContainerModel Model { get; private set; }
7:
8: /// <summary>
9: /// Gets a list which will be used to track which items have been bound.
10: /// </summary>
11: public List<ItemModel> BoundItems { get; private set; }
12:
13: public ContainerModelBinder()
14: {
15: BoundItems = new List<ItemModel>();
16: }
17:
18: protectedoverrideobject CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
19: {
20: // Set the Model property so child binders can find children.
21: Model = base.CreateModel(controllerContext, bindingContext, modelType) as ContainerModel;
22:
23: return Model;
24: }
25: }

Next we will create the child binder and have it point to the parent binder to get instances of the child objects.  Note that this only works if there is only one property of type ItemModel in the parent class since the property to find the item in the parent is hard coded.

						1:  
						public
						class ItemModelBinder : DefaultModelBinder
2: {
3: /// <summary>
4: /// Gets the parent binder so we can find objects in the parent's collection
5: /// </summary>
6: public ContainerModelBinder ParentBinder { get; private set; }
7:
8: public ItemModelBinder(ContainerModelBinder containerModelBinder)
9: {
10: ParentBinder = containerModelBinder;
11: }
12:
13: protectedoverrideobject CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
14: {
15: // Find the item in the parent collection and add it to the bound items list.
16: ItemModel item = ParentBinder.Model.Items.FirstOrDefault(i => !ParentBinder.BoundItems.Contains(i));
17: ParentBinder.BoundItems.Add(item);
18:
19: return item;
20: }
21: }

Finally, we will register these binders in Global.asax.cs so they will be used to bind the classes.

						1:  
						protected
						void Application_Start()
2: {
3: AreaRegistration.RegisterAllAreas();
4:
5: ContainerModelBinder containerModelBinder = new ContainerModelBinder();
6: ModelBinders.Binders.Add(typeof(ContainerModel), containerModelBinder);
7: ModelBinders.Binders.Add(typeof(ItemModel), new ItemModelBinder(containerModelBinder));
8:
9: RegisterRoutes(RouteTable.Routes);
10: }

I'm sure some of my fellow geeks will comment that this could be done more efficiently by simply rewriting some of the methods of the default model binder to get the same desired behavior.  I like my method shown here because it extends the binder class instead of modifying it so it minimizes the potential for unforseen problems.

In a future post (if I ever get around to it) I will explore creating a generic version of these binders.

Posted On Tuesday, November 29, 2011 5:29 PM | Comments (1) |

Powered by: