Introduction
This is a step by step guide to use a custom N-Tier Framework based on Linq to SQL and/or Linq to XML. This framework of abstract
classes and interfaces should do the repeating work so you can focus on the business logic.
In the attached solution, you can find a Console project to create the test-database. Don't forget to customize the Config-file in both the Console and the Website project!
Background
The reason I started this project was that I got tired of repeating the same stuff. I tried to make a template solution that I could use for other future projects. It was not my intention to automatise everything but to still give as much control as possible to the developer.
Using the Code
Steps to add a new Item:
- Project EntityModel
- Project Core
- Create a Business Object
- Create a SearchObject (optional)
- Modify SearchObjectFactory
- Project DAL
- Create a Service
- Modify ServiceFactory
- Project BLL
- Create a Manager
- Modify ManagerFactory
- Website.Core (optional)
- Website
- Create a ListControl
- Create a DetailControl
- Create Pages
1. Project EntityModel
Drag or add a new class to your DataContext
and implement the ILinqEntity
interface (required):
partial class Item : ILinqEntity {
}
2. Project Core
2.1 Create a new Business Object, inherit from
EntityBase
(required) and implement interfaces (optional):
public class Item : EntityBase, IEntityWithDescription, IEntityWithOnlineCheck {
public string Title { get; set; }
public string Description { get; set; }
public decimal? Price { get; set; }
public bool Online { get; set; }
public object SectionId { get; set; }
public Section Section { get; set; }
public Item()
: base() {
Online = true;
}
}
Possible interfaces:
IEntityWithTitle
IEntityWithDescription
IEntityWithSortOrder
IEntityWithOnline
2.2 Create a new SearchObject
(optional), inherit from SearchObjectBase
(required) and implement interfaces (optional).
The SearchObject is some kind of wrapper around a Dictionary that is extendable to other types of SearchObjects. This will enable automated joins in the DAL later. It can also fill its parameters from a QueryString-like string which will be useful in the usercontrols.
public enum SortItemsBy
{
DateInputDesc = 0,
TitleAsc = 1,
TitleDesc = 2,
PriceAsc = 3,
PriceDesc = 4,
DateInputAsc = 5,
DateUpdateAsc = 6,
DateUpdateDesc = 7,
Nothing = 8
}
public class ItemSearchObject : SearchObjectBase,
ISearchObjectWithOnline, ISearchObjectWithParentId
{
protected override string Prefix {
get { return "i_"; }
}
public object ParentId {
get { return SectionSearchObject.ItemId; }
set { SectionSearchObject.ItemId = value; }
}
public decimal? PriceMin {
get { return GetDecimal("PriceMin"); }
set { SetDecimal("PriceMin", value); }
}
public decimal? PriceMax {
get { return GetDecimal("PriceMax"); }
set { SetDecimal("PriceMax", value); }
}
public bool? Online {
get { return GetBoolean("Online"); }
set { SetBoolean("Online", value); }
}
public SortItemsBy SortBy {
get { return (SortItemsBy)(GetInt("sort") ?? 0); }
set {
if (value != SortItemsBy.DateInputDesc)
SetInt("sort", (int)value);
else
SetInt("sort", null);
}
}
public override bool HasParameters {
get {
return base.HasParameters
|| ParentId != null
|| PriceMin.HasValue
|| PriceMax.HasValue
|| Online.HasValue;
}
}
public override bool HasExtendedParameters {
get {
return HasParameters
|| HasSectionParameters;
}
}
public bool HasSectionParameters {
get { return SectionSearchObject.HasParameters; }
}
public SectionSearchObject SectionSearchObject {
get { return ExtendTo<SectionSearchObject>(); }
}
protected internal override string GetStringFromExtendedParameters() {
return SectionSearchObject.GetStringFromParameters();
}
Possible interfaces:
ISearchObjectWithOnline
ISearchObjectWithParentId
2.3 Modify the
SearchObjectFactory
class (required if you created a
SearchObject
).
Add the following lines:
else if (typeof(T) == typeof(Item))
return new ItemSearchObject();
3. Project DAL
3.1 Create a service, inherit from SqlServiceBase<T,Y>
or XmlServiceBase<T>
(required):
using Core;
using EntityModel;
using LinqItem = EntityModel.Item;
using CoreItem = Core.Item;
public class SqlItemService : SqlServiceBase<CoreItem, LinqItem>
{
SqlSectionService ss;
public SqlItemService()
: base() {
}
public SqlItemService(string connectionstring)
: base(connectionstring) {
}
internal SqlItemService(EntityDataContext dc)
: base(dc) {
}
public override CoreItem GetItem(object id) {
IQueryable<LinqItem> query = base.Table.Where(i => i.Id.Equals(id));
return base.GetItem(query);
}
protected internal override IQueryable<LinqItem>
FilterQuery(IQueryable<LinqItem> query, ISearchObject so) {
ItemSearchObject iso = so.ExtendTo<ItemSearchObject>();
if (iso.ItemId != null)
query = query.Where(i => iso.ItemId.Equals(i.Id));
if (iso.PriceMin.HasValue)
query = query.Where(i => i.Price >= iso.PriceMin.Value);
if (iso.PriceMax.HasValue)
query = query.Where(i => i.Price <= iso.PriceMax.Value);
if (iso.Online.HasValue)
query = query.Where(i => iso.Online.Value.Equals(i.Online));
if (!String.IsNullOrEmpty(iso.Keywords))
foreach (string keyword in iso.KeywordList) {
string kw = keyword;
query = query.Where(i => i.Title.Contains(kw) ||
i.Description.Contains(kw));
}
return query;
}
protected internal override IQueryable<CoreItem>
SortQuery(IQueryable<CoreItem> query, ISearchObject so) {
ItemSearchObject sso = so.CopyTo<ItemSearchObject>();
switch (sso.SortBy) {
case SortItemsBy.TitleAsc:
query = query.OrderBy(q => q.Title);
break;
case SortItemsBy.TitleDesc:
query = query.OrderByDescending(q => q.Title);
break;
case SortItemsBy.PriceAsc:
query = query.OrderBy(q => q.Price);
break;
case SortItemsBy.PriceDesc:
query = query.OrderByDescending(q => q.Price);
break;
case SortItemsBy.DateInputAsc:
query = query.OrderBy(q => q.DateInput);
break;
case SortItemsBy.DateInputDesc:
query = query.OrderByDescending(q => q.DateInput);
break;
case SortItemsBy.DateUpdateAsc:
query = query.OrderBy(q => q.DateUpdate);
break;
case SortItemsBy.DateUpdateDesc:
query = query.OrderByDescending(q => q.DateUpdate);
break;
}
return query;
}
protected internal override IQueryable<LinqItem>
GetParentItemsQuery(IQueryable<LinqItem> query, ISearchObject so) {
if (so.ExtendTo<ItemSearchObject>().HasSectionParameters) {
ss = new SqlSectionService(DataContext);
query = query.Join(
ss.GetQuery(so, FilterInclude.Parent),
i => i.SectionId,
s => s.Id,
(i, s) => i
);
}
return query.Distinct();
}
protected internal override LinqItem ConvertCoreEntity(CoreItem item) {
if (item == null)
return null;
LinqItem lItem = new LinqItem();
int itemId = 0;
if (item.Id != null)
Int32.TryParse(item.Id.ToString(), out itemId);
lItem.Id = itemId;
int parentId = 0;
if (item.SectionId != null)
if (Int32.TryParse(item.SectionId.ToString(), out parentId))
lItem.SectionId = parentId;
lItem.Title = item.Title;
lItem.Description = item.Description;
lItem.Price = item.Price;
lItem.Online = item.Online;
lItem.DateInput = item.DateInput;
lItem.DateUpdate = item.DateUpdate;
return lItem;
}
protected internal override CoreItem ConvertLinqEntity(LinqItem item) {
if (item == null)
return null;
CoreItem cItem = new CoreItem();
cItem.Id = item.Id;
cItem.SectionId = item.SectionId;
cItem.Title = item.Title;
cItem.Description = item.Description;
cItem.Online = item.Online;
cItem.DateInput = item.DateInput;
cItem.DateUpdate = item.DateUpdate;
return cItem;
}
protected internal override IQueryable<CoreItem>
ConvertLinqEntityQuery(IQueryable<LinqItem> query) {
return query.Select(i =>
new CoreItem {
Id = i.Id,
SectionId = i.SectionId,
Title = i.Title,
Description = i.Description,
Price = i.Price,
Online = i.Online,
DateInput = i.DateInput,
DateUpdate = i.DateUpdate
}
);
}
public override void Dispose() {
if (ss != null)
ss.Dispose();
base.Dispose();
}
}
3.2 Modify ServiceFactory
(required).
Add the following lines:
else if (typeof(T) == typeof(Item))
return new SqlItemService() as IService<T>;
4. Project BLL
4.1 Create a manager, inherit from ManagerBase<T>
(readonly) or ManagerModifiableBase<T>
(required), and implement interfaces (optional):
public class ItemManager : ManagerModifiableBase<Item>,
IManagerWithSearchObject<Item>
{
public Item GetItem(ISearchObject so) {
return base.GetItemBySearchObject(so);
}
public IList<item /> GetItems(ISearchObject so) {
return base.GetItemsBySearchObject(so);
}
public int GetCount(ISearchObject so) {
return base.GetCountBySearchObject(so);
}
protected override bool IsValidInput(Item item) {
return base.IsValidInput(item)
&& item.SectionId != null
&& !String.IsNullOrEmpty(item.Title)
&& !String.IsNullOrEmpty(item.Description);
}
}
Possible interfaces:
IManagerWithSearchObject<T>
IManagerCachable<T>
IManagerModifiable<T>
4.2 Modify ManagerFactory<T>
(required).
Add the following lines:
if (typeof(T) == typeof(Item))
return new ItemManager() as IManager<T>;
5. Website.Core
Modify UrlManager
(optional):
public static string CreateDetailUrl(Item item, QueryStringManager q) {
return createUrl("/ItemDetail.aspx", new ItemSearchObject()
{ ItemId = item.Id }, q);
}
public static string CreateListUrl(ItemSearchObject iso, QueryStringManager q) {
return createUrl("/ItemList.aspx", iso, q);
}
6. Website
6.1 Create a ListControl
This control will automatically filter the results based on the QueryString parameters:
<asp:Repeater ID="ItemRepeater" runat="server"
onitemdatabound="ItemRepeater_ItemDataBound">
<ItemTemplate>
<p>
<strong><asp:Literal ID="TitleLiteral" runat="server" /></strong>
(<asp:Literal ID="PriceLiteral" runat="server" />)
</p>
</ItemTemplate>
</asp:Repeater>
public partial class ItemList : ListControl<Item>
{
protected void ItemRepeater_ItemDataBound(object sender, RepeaterItemEventArgs e) {
Item item = e.Item.DataItem as Item;
Literal title = e.Item.FindControl("TitleLiteral") as Literal;
title.Text = "<a href=\"" + UrlManager.CreateDetailUrl(item, Q) + "\">"
+ StringHelper.StripHtml(item.Title) + "</a>";
Literal price = e.Item.FindControl("PriceLiteral") as Literal;
price.Text = StringHelper.ShowPrice(item.Price);
}
protected override void FillControl() {
ItemRepeater.DataSource = base.Items;
ItemRepeater.DataBind();
}
}
6.2 Create a DetailControl
This control will automatically get the item based on the id included in the QueryString parameters
<h3><asp:Literal ID="TitleLiteral" runat="server" /></h3>
<p><asp:Literal ID="DescriptionLiteral" runat="server" /></p>
<p><b>Price:</b> <asp:Literal ID="PriceLiteral" runat="server" /></p>
public partial class ItemDetail : DetailControl<Item>
{
protected override void FillControl() {
TitleLiteral.Text = base.Item.Title;
DescriptionLiteral.Text = base.Item.Description;
PriceLiteral.Text = StringHelper.ShowPrice(base.Item.Price ?? 0);
}
}
6.3 Create one or more Pages:
<p><i><asp:Literal ID="CountLiteral" runat="server" Text="No" /> item(s) found</i></p>
<uc1:ItemList ID="ItemListControl" runat="server" />
public partial class ItemListPage : CustomPage
{
protected override void AfterLoad() {
base.AfterLoad();
CountLiteral.Text = ItemListControl.TotalCount.ToString();
}
}
History
- 23rd September, 2010: Initial post