Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
West Wind WebSurge - Rest Client and Http Load Testing for Windows

A simple Business Object wrapper for LINQ to SQL


:P
On this page:

I've been talking about how I'm using a small business object wrapper around LINQ to SQL to provide better abstraction of the LINQ to SQL functionality and I suppose the time's come to be bit more specific. This is going to be a long post with a fair bit of code and a brief discussion of the whys and hows. So I want to share my ideas of what I'm thinking here and an overview of what I've built so far. It's not complete, but enough to work with to get a feel for it and I'm using it successfully with a sample app I'm building for my ASP.NET Connections session. Granted it's not a complicated app (a Time Trakker) but I have to admit - especially given my original misgivings and skepticism towards LINQ to SQL - it's working out pretty damn well.

I'm throwing this out now to get some thoughts and feedback and I'll post my current code so any so inclined can play around with this, but keep in mind this is rougher than stuff I usually put out publicly so it may take some twiddling.

So... where to start?

LINQ to SQL is essentially an OR/M tool that requires modeling your data model to an object model. But while you are using objects, in essence LINQ to SQL still is little more than a fancy Data Access Layer. It does not make a business object layer or middle tier.

It seems these days the concept of business objects is getting buried somewhat under the veil of technology. I was reminded of that the other day while reading a post on Rocky's blog regarding CSLA in comparison Enterprise Framework. We are so absorbed by the discussion of the technological choices that the business and logistical aspects and separation of responsibility often just seems to get lost. It's rare these days to hear people talking about business objects anymore - the focus is all on the front end and the data access technology itself which is kinda sad, because in my mind at least business objects really provide the biggest productivity and stability boost in app development.

My goal with what I'm building is not to build an end all framework - I've never been a fan of big frameworks, but ever since the dawn of time it seems I've built myself a small set of business object wrappers that facilitate the process of building data centric applications. And this is just one more iteration of it. This is nowhere as complete as CSlA or Mere Mortals or some other framework would be but it is also smaller and easier to work with. In time, I'm sure bigger frameworks will integrate with LINQ to SQL or Entity Framework too, but right now if you want to use this technology you have to roll your own. Again, this is my vision for my needs and I'm simply sharing it here - I'm not trying to get into a holy war on which approach is better <s>.

So, Business Objects in my mind serve several main purposes (at a minimum):

Logical Abstraction
They abstract the object model in such a way that all code related to accessing the object model (in this case LINQ to SQL Entities) as well as any other incidental data access in one place. While an object model may map database tables and relationships 1 - 1 , the business layer can combine these objects in any combination necessary to provide the logical high level abstraction. The typical example here is an invoice object that controls Invoice header, line items and customers for example. The invoice knows how to access all of these components and its the business object that accesses the model not the front end code. Front end code typically interacts with the business object and receives result data (queries or data objects) or in the case of CRUD operations entity objects to work with in the UI.

Code Abstraction
Just as important as the logical abstraction is the code abstraction in that all code related to a business object ends up in one place. This may be a single class or a set of classes, but there's a distinct place where are 'business logic' is addressed - always in one place. Never does the front end code talk to the data directly (although with LINQ there's some blurring of this line because it gets a little fuzzy of what 'data' means when you're dealing with LINQ expressions) it only sees the results which are put together by the business object. One huge advantage with this scenario is that you can easily reuse business object code in different applications or even different parts of your application. So if you build an ASP.NET page that access the business object, you use the same business object you might use a in a Web Service or even a Windows Forms application. This sort of code reuse simply cannot happen if you stick any sort of data access code into the UI using DataSource controls <g>.

CommonBusiness Object and Data Functionality
LINQ to SQL provides a pretty easy data access model through the LINQ to SQL entity classes and it's pretty straight forward. A business layer built ontop of LINQ to SQL can certainly take advantage of this functionality and reduce the amount of code that needs to get written to provide the basic DAL functionality (which in non-OR/M environments must be built separately). With LINQ to SQL a business layer can leverage that functionality directly and even provide much additional value for data retrieval operations by returning results as LINQ queries as I mentioned yesterday.

But there are also many things missing that a business layer needs. Validation for example, is not really something that you can or should handle on the entity model itself. LINQ to SQL does provide OnValidate methods for each mapped property it creates, but this sort of mechanism is very difficult to consolidate in a unified validation strategy. So at minmium there should be a ValidationErrors facility that can be checked and used to ask a business object to validation itself with a simple Validate() method perhaps.

Loading and Saving too should be simpler for front end code than what LINQ to SQL provides. Realistically UI code should have to know very little about the data it needs to access and certainly for simple tasks like retrieving an entity instance you should just be able to provide a key or potentially call specialized bus object methods that return an entity. This translates into standard Load(pk) and Save() methods that know what to do without any further setup.

Given that LINQ to SQL has some 'issues' with disconnected operation that model should also be abstracted. In my simple framework I have an option flag on the business object that allows either connected or disconnected operation. By default the same connected mode that LINQ to SQL uses is used where a DataContext instance jacked to the business object holds the context's change state. Any Save Operation then simply calls submit changes. In disconnected mode the business object creates a new context for each operation - to feed entities out and to save them back. The connected mode is more efficient, but if you need to disconnect it's easily done or can be overridden for the default behavior altogether when the object is created.

Finally LINQ to SQL provides essentially DAL functionality - but it doesn't provide much in the way of core ADO.NET operations which in some situations might be handy. There's no direct support for executing a SQL command and returning a DataReader or DataTable for example. There's not a lot of need for this functionality, but I've found that at times it's damn handy to be able to return data in otherways. So there's an extension of the DataContext that provides the abillity to easily create commands, parameters, and run database queries and commands more easily.

There's also a Converter class on the business object that can take a query (generated through this same business object's DataContext) and convert it into a resultset other than an EntityList. This it turns out is pretty important in that data binding to good old ADO.NET types is much more efficient than binding to entity lists which require Reflection to bind each data item.

A simple Business Framework for LINQ to SQL
So without much further ado here's what I've built so far. Keep in mind that this is just a start although I'm finding that what I have so far is quite adequate to work with.

Here's a a class diagram (with the hacked markup since VS can't deal with the generic associations) that shows what the object model looks like:

wwBusinessObject

From a usability point of view compared to LINQ to SQL there's basically just one extra level of indirection: The business object. So to use this model, you create a new business object that inherits from wwBusinessObject<TEntity, TContext> and you provide the generated DataContext and EntityType from your LINQ to SQL model as generic parameters.

For example:

public class busEntry : wwBusinessObject<EntryEntity, TimeTrakkerContext>
 

A  DataContext is created for each business object and it manages its own context. So two business object instances don't share the same context state.

The TEntity generic parameter is used for providing a link to the 'main' Entity that is associated with the business object. The business object may deal with more entities internally but there's usually going to be a primary entity that drives the BO. The primary entity is what is updated for the built in CRUD methods, so when you call Load() you get an instance of the provided entity type.

Note that the business object also has an internal Entity member which is loaded by each of the load operations. I've always found this very convenient because it makes it easy to pass around entity data in the context of a page without having to create separate instances. Load() and NewEntity() will automatically set the internal entity member, but also return the entity as return value. The .Entity property is merely a convenience.

For CRUD operations the business object doesn't require any setup or configuration code. Simply the above definition is enough to use code like the following:

this.Entry = new busEntry();
 
if (this.Entry.Load(10) == null)  // load by pk
    return false;
 
this.Entry.Entity.Title = "My Entry";
 
if (!this.Entry.Save())
{
    this.SetError(this.Entry.ErrorMessage);
    return;
}
 
this.Entry.NewEntity();
this.Entity.Title = "New Entity";
this.Entity.Save();
 
// *** Still pointing at new entity
this.Entry.Entity.Title = "Something else";
 
// *** Delete the entity
if (this.Entry.Delete())
    this.SetError("Couldn't delete: " + this.Entry.ErrorMessage);
 

This may not seem like a big improvement over LINQ to SQL, but it's actually a lot less code than you'd have to write even with plain LINQ to SQL code and it handles a fair amount of details behind the scenes such as error handling.

Here the client code is not talking directly to the data model to retrieve or save data - all it does is interact with the entity and then call the business object 'to deal with it'. No LINQ syntax for any of this which is as it should be IMHO. The UI doesn't talk to the data or even the model directly, only to the entities.

Query methods in the business object (such as GetRecentEntries() for example) generally are implemented as methods that return Query objects. Preferrably you'd want to return strongly typed entities like this bus object method:

/// <summary>
/// Get open entries for a given user
/// </summary>
/// <param name="userPk"></param>
/// <returns></returns>
public IQueryable<EntryEntity> GetOpenEntries(int userPk)
{
    IQueryable<EntryEntity> q = 
        from e in this.Context.EntryEntities
        where !e.PunchedOut
        orderby e.TimeIn
        select e;
 
    // *** Add filter for User Pk - otherwise all open entries are returned
    if (userPk > 0)
        q = q.Where(e => e.UserPk == userPk);
 
    return q;
}

So that the front end code can optionally further filter the query. When a strongly typed query is returned there are a lot of options to deal with the data as shown in the following snippet:

 
protected override void OnInit(EventArgs e)
{
    base.OnInit(e);
 
    // *** Set up the base query
    IQueryable<EntryEntity> entries = this.entry.GetOpenEntries(this.TimeTrakkerMaster.UserPk);
 
    int count = entries.Count();
    if (count == 1)
    {                
        int? Pk = entries.Select(en => en.Pk).FirstOrDefault();
        if (Pk == null)
            Response.Redirect("~/Default.aspx");
 
        Response.Redirect("~/punchout.aspx?id=" + Pk.ToString());
    }
 
    // *** Assign the data source - note we can filter the data here!
    this.lstEntries.DataSource = this.entry.Converter.ToDataReader(
entries.Select(en => new { en.Pk, en.Title, en.TimeIn }));
 
    this.lstEntries.DataBind();
}

All the queries that actually hit the database are shown in bold. Notice that there are 3 different queries that are run from the original query returned from the business object, giving the front end code a ton of control of how to present the data in the UI.

Notice also the entry.Converter.ToDataReader() call for databinding. This isn't strictly necessary - you could directly bind the result of the query. However, databinding to a DataReader() is 3-4 times faster than binding to an entity list as Entity list binding requires use of Reflection for each data item. The DataConverter provides an easy way to convert to DataReader, DataTable (useful for paging, editing and still way faster binding than Entities) and List. Another advantage of the Converter is that it fires any errors into the business object so errors can be trapped more effectively.

Ok... so the CRUD code above is in connected mode. If you want to run CRUD operations in disconnected this should work:

// *** You can also work on the entities disconnected
this.Entry.Options.TrackingMode = Westwind.BusinessFramework.TrackingModes.Disconnected;            
 
EntryEntity entry = this.Entry.Load(10);
entry.Title = "Updated Title";
this.Entry = null;   // kill business object
 
// *** Create a new one
this.Entry = new busEntry();
this.Entry.Options.TrackingMode = Westwind.BusinessFramework.TrackingModes.Disconnected;
this.Entry.Save(entry);  // update disconnected entity

To give you an idea of what this looks like in a Web page here's some code handles displaying and then saving entity data for a new time entry:

protected override void OnInit(EventArgs e)
{
    base.OnInit(e);
 
    this.Proxy.TargetCallbackType = typeof(Callbacks);  // wwMethodCallback Ajax Callbacks handler
 
    if (this.Proxy.IsCallback)
        return;
 
    object projectQuery = Project.GetOpenProjects();
 
 
    this.lstProjects.DataSource = Project.Converter.ToDataReader(projectQuery);
    this.lstProjects.DataValueField = "Pk";
    this.lstProjects.DataTextField = "ProjectName";
    this.lstProjects.DataBind();
 
    object customerQuery = Customer.GetCustomerList();
 
    this.lstCustomers.DataSource = Customer.Converter.ToDataReader(customerQuery); 
    this.lstCustomers.DataTextField = "Company";
    this.lstCustomers.DataValueField = "Pk";
    this.lstCustomers.DataBind();
}

protected
override void OnLoad(EventArgs e)
{
    base.OnLoad(e);
 
    if (this.Proxy.IsCallback)
        return;
 
    this.TimeTrakkerMaster.SubTitle = "Punch In New Entry";
 
    if (this.Entry.NewEntity() == null)
    {
        this.ErrorDisplay.ShowError("Unable to load new Entry:<br/>" + this.Entry.ErrorMessage);
        return;
    }
 
    if (!this.IsPostBack)
    {
        // *** Get the User's last settings
// we need to load user here no association yet
        busUser user = TimeTrakkerFactory.GetUser();
        if (user.Load(this.TimeTrakkerMaster.UserPk) != null)
       {                    
            if ( user.Entity.LastCustomer > 0 )
                this.Entry.Entity.CustomerPk = user.Entity.LastCustomer;
            if (user.Entity.LastProject > 0)
                this.Entry.Entity.ProjectPk = user.Entity.LastProject;
        }
 
        // *** Now bind it
        this.DataBinder.DataBind();
    }
}
 
 
protected void btnPunchIn_Click(object sender, EventArgs e)
{
    // *** Start by unbinding the data from controls into Entity
    this.DataBinder.Unbind();
 
    // *** Manual fixup for the split date field
    DateTime PunchinTime = Entry.GetTimeFromStringValues(this.txtDateIn.Text, this.txtTimeIn.Text);
    if (PunchinTime <= App.MIN_DATE_VALUE)
    {
        this.DataBinder.BindingErrors.Add( new Westwind.Web.Controls.BindingError("Invalid date or time value", this.txtDateIn.ClientID));
        Entry.ValidationErrors.Add("Invalid date or time value", this.txtTimeIn.ClientID);
    }
    Entry.Entity.TimeIn = PunchinTime;
 
    // *** Validate for binding errors - and error out if we have any
    if (this.DataBinder.BindingErrors.Count > 0)
    {
        this.ErrorDisplay.ShowError(this.DataBinder.BindingErrors.ToHtml(), "Please correct the following:");
        return;
    }
 
    // *** Have to make sure we associate a user with this entry
    Entry.Entity.UserPk = this.TimeTrakkerMaster.UserPk;
 
 
    // *** Validate business rules
    if (!this.Entry.Validate())
    {
        foreach (ValidationError error in this.Entry.ValidationErrors)
        {                    
            this.DataBinder.AddBindingError(error.Message,error.ControlID);
        }
        this.ErrorDisplay.ShowError(this.DataBinder.BindingErrors.ToHtml(), "Please correct the following:");
        return;
    }
 
 
    // *** Finally save the entity
    if (!this.Entry.Save())
        this.ErrorDisplay.ShowError("Couldn't save entry:<br/>" +
                                    this.Entry.ErrorMessage);
    else
    {
        this.ErrorDisplay.ShowMessage("Entry saved.");
        Response.AppendHeader("Refresh", "2;default.aspx");
 
        // *** Remember last settings for Project and Customer for the user
        // *** NOTE: Entry.Entity.User is not available here because it's a NEW record
        //           so we explicitly load and save settings
        busUser User = TimeTrakkerFactory.GetUser();
        User.SaveUserPreferences(Entry.Entity.UserPk, Entry.Entity.CustomerPk, Entry.Entity.ProjectPk);
    }
}

This is one of the simpler examples that deals mostly with a single business object but it should give you an idea of how things work. I mentioned the query functionality yesterday.

The business object's typical implementation will likely provide:

Query Result Methods
Methods that return data that is used in the front end. These are methods like GetOpenProjects() or GetCustomerList() that generally take input parameters and then create Queries that get returned as a result. Remember queries are not executed until enumerated so effectively no SQL access occurs until databinding happens.

Overridden CRUD operations
This is actually quite common: You'll want to set default values or perform other operations on new entities, set default values (say an Updated column) on or before saving. This can also mean speciaty methods that basically overload CRUD operations. For example in Time Trakker I have things like PunchIn and PunchOut that are essentially overloads of the Save() method that assign specific values first. For my user object I have AuthenticateAndLoad(string username, string password) which is essentially an overloaded Load() method.

Convenience Methods
Often you also have convenience methods that format data a certain way or perform special taks in batch on the entity object for example. Or you may have special operations that run updates against the database. Maybe a 'UpdateTimeTotals()' that ensures that all data in the tables are properly calculated (which incidentally would be a non-LINQ operation using just a command object).

Again to give you an idea of what this looks like here's my business object for the Entry object:

 
    /// <summary>
    /// Business object related a time entry.
    /// </summary>
    public class busEntry : wwBusinessObject<EntryEntity, TimeTrakkerContext>
    {
        /// <summary>
        /// Get open entries for a given user
        /// </summary>
        /// <param name="userPk"></param>
        /// <returns></returns>
        public IQueryable<EntryEntity> GetOpenEntries(int userPk)
        {
            IQueryable<EntryEntity> q = 
                from e in this.Context.EntryEntities
                where !e.PunchedOut
                orderby e.TimeIn
                select e;
 
            // *** Add filter for User Pk - otherwise all open entries are returned
            if (userPk > 0)
                q = q.Where(e => e.UserPk == userPk);
 
            return q;
        }
 
        /// <summary>
        /// Get all open entries
        /// </summary>
        /// <returns></returns>
        public IQueryable<EntryEntity> GetOpenEntries()
        {
            return this.GetOpenEntries(-1);
        }
 
        /// <summary>
        /// Gets a list of recent entries 
        /// </summary>
        /// <param name="userPk"></param>
        /// <param name="Count"></param>
        /// <returns></returns>
        public IQueryable<EntryEntity> GetEntries(int userPk)
        {
            IQueryable<EntryEntity> q =
                from e in this.Context.EntryEntities                
                orderby e.TimeIn descending
                select e;
 
            return q;
        }
 
 
 
#region   overridden CRUD operation
        /// <summary>
        /// Sets default time value
        /// </summary>
        /// <returns></returns>
        public override EntryEntity NewEntity()
        {
            EntryEntity entry = base.NewEntity();
            if (entry == null)            
                return null;
 
            entry.TimeIn = TimeUtilities.RoundDateToMinuteInterval(DateTime.Now,
                                                                   App.Configuration.MinimumMinuteInterval,
                                                                   RoundingDirection.RoundUp);
            entry.TimeOut = App.MIN_DATE_VALUE;            
 
            return entry;
        }
 
        /// <summary>
        /// Fixes up times for Universal Time to the database
        /// </summary>
        /// <returns></returns>
        public override bool Save()
        {
            //if (this.Entity.TimeIn != null)
            //    this.Entity.TimeIn = this.Entity.TimeIn.ToUniversalTime();            
            //if (this.Entity.TimeOut != null)
            //    this.Entity.TimeOut = this.Entity.TimeOut.ToUniversalTime();
 
            return base.Save();
        }
 
        /// <summary>
        /// Fixes up times for local time from the database
        /// </summary>
        /// <param name="pk"></param>
        /// <returns></returns>
        public override EntryEntity Load(object pk)
        {
            if (base.Load(pk) == null)
                return null;
 
            //if (this.Entity.TimeIn != null)
            //    this.Entity.TimeIn = Entity.TimeIn.Value.ToLocalTime();
 
            //if (this.Entity.TimeOut != null)
            //    this.Entity.TimeOut = Entity.TimeOut.Value.ToLocalTime();
 
            return this.Entity;
        }
 
 
        /// <summary>
        /// Checks for empty title and time in values and associations for user, customer and project
        /// </summary>
        /// <returns></returns>
        public override bool Validate()
        {
            base.Validate();
 
            if (string.IsNullOrEmpty(this.Entity.Title))
                this.ValidationErrors.Add("The title is required","txtTitle");
 
            if (this.Entity.TimeIn <= App.MIN_DATE_VALUE)            
                this.ValidationErrors.Add("Time and/or date value is invalid","txtTimeIn");
 
            if (this.Entity.CustomerPk < 1)
                this.ValidationErrors.Add("A customer must be associated with this entry", "txtCustomerpk");
 
            if (this.Entity.ProjectPk < 1)
                this.ValidationErrors.Add("A project must be associated with this entry", "txtProjectPk");
 
            if (this.Entity.UserPk < 1)
                this.ValidationErrors.Add("A user must be associated with this entry", "txtUserPk");
 
            if (ValidationErrors.Count > 0)
                return false;
 
            return true;
        }
 
        /// <summary>
        /// punches out an individual entry and saves it
        /// </summary>
        /// <returns></returns>
        public bool PunchOut()
        {
            this.Entity.PunchedOut = true;
            if (this.Entity.TimeOut == null || this.Entity.TimeOut < this.Entity.TimeIn)
                this.Entity.TimeOut = DateTime.Now;
 
            return this.Save();
        }
 
        /// <summary>
        /// Punches in a new entry by setting punch in time
        /// </summary>
        /// <returns></returns>
        public bool PunchIn()
        {
            return this.PunchIn(null);
        }
 
        /// <summary>
        /// Punches in a new entry by setting punch in time
        /// </summary>
        /// <param name="entity"></param>
        /// <returns></returns>
        public bool PunchIn(EntryEntity entity)
        {
            this.Entity.PunchedOut = false;
 
            if ( this.Entity.TimeIn <= App.MIN_DATE_VALUE )
                this.Entity.TimeIn = DateTime.Now;
 
            if (Entity == null)
                return this.Save();
            else
                return this.Save(entity);
        }
 
        /// <summary>
        /// Punches out an individual entry and saves it
        /// </summary>
        /// <param name="entry"></param>
        /// <returns></returns>
        public bool PunchOut(EntryEntity entry)
        {
            entry.PunchedOut = true;
 
            if (entry.TimeOut == null || entry.TimeOut < entry.TimeIn)
                entry.TimeOut = DateTime.Now;
 
            this.CalculateItemTotals();
 
            return this.Save(entry);
        }
#endregion
 
        /// <summary>
        /// Utility function that converts a date time entry value from
        /// a date and time string to a DateTime value
        /// </summary>
        /// <param name="Date"></param>
        /// <param name="Time"></param>
        /// <returns></returns>
        public DateTime GetTimeFromStringValues(string Date, string Time)
        {
            DateTime val = App.MIN_DATE_VALUE;
            DateTime.TryParse(Date + " " + Time,out val);
 
            return val;
        }
 
        /// <summary>
        /// Calculates Item and Rate totals and sets it on the passed entry object
        /// </summary>
        public void CalculateItemTotals(EntryEntity Entry)
        {
            if (Entry == null)
                Entry = this.Entity;                       
 
            if (Entry.TimeIn == null || 
                Entry.TimeOut== null || 
                Entry.TimeOut < Entry.TimeIn)
                Entry.TotalHours = 0.00M;
            else if ( Entry.TimeOut > App.MIN_DATE_VALUE && 
                      Entry.TimeIn > App.MIN_DATE_VALUE)
                Entry.TotalHours = (decimal)Entry.TimeOut.Subtract(Entry.TimeIn).TotalHours;
 
            Entry.ItemTotal = Entry.TotalHours * Entry.Rate;
        }
 
 
        /// <summary>
        /// Calculates Item and Rate totals. This version works off the internal Entity object
        /// </summary>
        public void CalculateItemTotals()
        {
            this.CalculateItemTotals(null);
        }
 
        /// <summary>
        /// Adjusts the time values for rounding conditions
        /// </summary>
        public void RoundTimeValues()
        {
            if (this.Entity.TimeIn > App.MIN_DATE_VALUE)
                this.Entity.TimeIn = TimeUtilities.RoundDateToMinuteInterval(this.Entity.TimeIn,
                        App.Configuration.MinimumMinuteInterval,
                        RoundingDirection.RoundUp);
            if (this.Entity.TimeOut > App.MIN_DATE_VALUE)
                this.Entity.TimeOut = TimeUtilities.RoundDateToMinuteInterval(this.Entity.TimeOut,
                       App.Configuration.MinimumMinuteInterval,
                       RoundingDirection.RoundUp);
        }
 
    }

Most of the methods are pretty minimalistic - as you would expect in a simple application like this. But it should give a pretty clear view of what typically happens in the business object. The code essentially deals with the core data manipulation, pre and post processing and of course generation of queries. The code tends to break down into very clearly defined responsibilities that are easy to test against. Further given the structure of the business object you have a sort of template that you follow with most business objects. You implement Validate() and probably one more Load() related methods and possibly Save() if there's some special save syntax.

The business object uses LINQ to SQL for data access and passes out either LINQ queries (IQueryable<T> for 'adjustable' results or IQueryable for fixed non-mutable results) or returns individual entities for the CRUD methods. In simple applications like the time tracking app I'm building nearly 90% of the code deals with CRUD and convenience methods and just a few simple queries which is quite common for transactional applications.

Most operations require relatively little amounts of code because of the high level of abstraction that LINQ offers. From a pure code usability perspective LINQ to SQL is making code cleaner and resulting in quite a bit less of it. So far I haven't run into any of my own 'warning' points - mainly because I haven't gotten there yet. I know I will have issues when I get to the Web Service portion of things and already had to dodge one issue when using AJAX serialization. But other than that so far so good.

So there you have it <g>. I'd be interested to hear thoughts.

[ 2/5/2008 code updated for .NET 3.5 RTM  ]

Grab the code

Posted in ADO.NET  ASP.NET  LINQ  

The Voices of Reason


 

J. PHILIP
September 27, 2007

# re: A simple Business Object wrapper for LINQ to SQL

Rick,

Shouldn't also the business object be data provider independent ?
The UI code should no have to change if you want to change data store, right ?
The nice thing with the entity classes generated by the OR tool is they are not provider specific.
So maybe have a wwBusinessObject<TEntity> and a Business Object service that fills it up in each provider's specific way.
I know that this is a Business Object wrapper for LINQ to SQL, but it seems it would not take much to add the option to change provider later if needed.

Rick Strahl
September 27, 2007

# re: A simple Business Object wrapper for LINQ to SQL

Yeah it should, but if you're using LINQ to SQL that's not an option at the moment, which is maybe one of the big shortcomings of LINQ to SQL. I suspect though that other providers will be supported in the future but for now it's SQL Server only.

IAC, provider independence is over-rated <s>. How often do you build applications that change providers. It happens but it's rare. If you're an MS shop chances are you're using SQL Server or SQL Express and certainly for the small to mid level scenario that LINQ to SQL addresses this is even more likely.

While you could use the generated entities in the model, generating your own data for them and doing the change management would all be required in somehting custom if you want to continue to use the LINQ syntax. If it comes to that you're going to be better off with some other non LINQ to SQL tool like nHibernate, LLBGen, Subsonic, WilsonOR etc.

yaip
September 27, 2007

# re: A simple Business Object wrapper for LINQ to SQL

Apart from OR/M diagrams, what am I missing if I use TableAdapters instead?

J. PHILIP
September 27, 2007

# re: A simple Business Object wrapper for LINQ to SQL

Rick,
OK, sorry, I had mostly read the introduction of the post, not so much the code.
I see the UI in the demo makes direct calls to Linq to SQL (The DAL), so a provider agnostic business layer would not work in this case.
Doesn't this go against the principles stated in the post?
Logical Abstraction
They abstract the object model in such a way that all code related to accessing the object model (in this case LINQ to SQL Entities) as well as any other incidental data access in one place...

Scott Allen
September 27, 2007

# re: A simple Business Object wrapper for LINQ to SQL

I like the looks of this, I'm going to download the code, but here are a couple questions:

1) By giving each entity its own data context, how would you insert, for example, an Entry and a User in transaction?

2) Are you seeing any connections leaking?

Rick Strahl
September 27, 2007

# re: A simple Business Object wrapper for LINQ to SQL

@J. Philip - Yes and no. The base queries originate in the BO. But you can take advantage of further filtering in the UI of the queries. The difference is that base query ALWAYS originates in the BO and you can (but certainly don't have to) use the further LINQ syntax to winnie down the result to exactly what you want in the front end UI. Although that does go to some degree against the "don't screw with the DAL" I think this is one of the big wins of using LINQ to SQL as it allows a ton of flexibility to the client in post format the data exactly how it's needed in the UI without having to have several BO method overloads to serve exact versions.

The alternative is that you never pass out queries out of the BO layer but only concrete instances with ToList() or whatever. But I think that would be denying one of the most compelling features of LINQ IMHO.

Rick Strahl
September 27, 2007

# re: A simple Business Object wrapper for LINQ to SQL

@Scott - The instancing is tied to the business object and the context goes away when the business object dies. I haven't checked closely into this yet and I've been thinking about exactly what the life time issues are. I know connections are not leaking because all data access is atomic and from some quick stress tests I don't see any sort of major memory leakage.

For clarfication - I'm not Disposing() the DataContext explicitly and I'm relying on the object going out of scope.

Rick Strahl
September 27, 2007

# re: A simple Business Object wrapper for LINQ to SQL

@Scott - Each business object has its own data context, not each entity. If you are dealing with say an Entry entity in the example, the Entity has access to all related entities and if necessary in the business layer code can access the context to load others.

The idea is that a business itself is mostly self contained in its use of business logic even if it spans multiple entities which it uses internally. You can also assign the data context explicitly with one of the constructors which then results in a shared DataContext but that's something that has to be explicit.

It might be interesting to think of a way to have a more global DataContext store, but the question in this scneario always becomes where do you hold this context then? If you need to share on a page now you could create a context on the page, then explicitly use the constructor with the context for each business object loaded.

J. PHILIP
September 27, 2007

# re: A simple Business Object wrapper for LINQ to SQL

Rick,

I like the idea of the concrete instances with ToList() for a provider agnostic BO as it also makes sure the Query is not executed twice. You could then query the list if needed with Linq to objects I guess.

On the validation, is there a way you can get automatic client side validation too using one the client side libraries like it is done in Monorail? This may be hard with web forms, but easier in the upcoming MVC framework.

Note on this comment box:
When I post a comment originating from a get request, the code is always rejected, so I always have to post it twice.

Jeff
September 28, 2007

# re: A simple Business Object wrapper for LINQ to SQL

I have been following your ups and downs with Linq for past several weeks and its nice to see you have found an application for it. However, I can't still cannot see the benefit if using Linq over any other well defined DAL. It seems to me, if anything its added complexity to a pretty simple problem (other than bringing data transformation from the db to the application layer). <p>
I guess if you did not have a DAL, it would suffice for that requirement. Still, accept for the ability to filter collections into subsets, I can't see the benefit of Linq vs. DTO and DAOs. Of, course it is newer, and thus perceived as sexier...So, what I am missing? Do I need more marketing effort from MS and Guthrie to buy into this?
</p>
BTW - been reading your articles and blog entries since 2001 and love your "say it like it is" approach. Keep up the good work!

Rick Strahl
September 28, 2007

# re: A simple Business Object wrapper for LINQ to SQL

Jeff no argument from me. If you're already using a well defined DAL LINQ will not offer a huge benefit, although I think that the abillity to use objects vs. DataSets is definitely an improvement in general usability.

As I mentioned previously I've been using a custom hybrid entity business object framework I built for my own long ago. It does a good chunk what the above does without LINQ to SQL. But LINQ to SQL adds:

* Much better entity management/relationship mapping
* completely wrapped DAL for CRUD (I could throw out all the DAL code)
* LINQ Queries for passing around

The first and last points are enough to make this worthwhile for me. The latter approach is saving a lot of coding I'm finding as I have an easy way to pass base queries around both the middle tier and front end.

I guess by building this sort of abstraction layer I'm trying to take some of the complexities and inconsistency out of the raw LINQ to SQL interface. You pretty much need to do that anyway with just about any data access strategy tool you use either by yourself (ie. homegrown) or using some library ontop of it, except that LINQ to SQL can stand on its own a lot easier than say using typed datasets and table adapters for example.

I think the biggest detriment AGAINST using LINQ to SQL (or EF for that matter) is the whole modelling aspect and keeping database and model in synch. The reason LINQ to SQL is interesting IMHO is because the model designer is relatively easy to work with and it's easy to manage the model even if you need to redrop tables relationships etc. But it's easy in my situation where I have full control over both code and database, but in situations where the database is under DBA control and changing frequently I can only imagine the headaches that come from trying to keep things in sync. Once you're not manually tracking what's in the database how in the heck do you sync back up short of regenerating everything?

The DevTeach discussion between Ted Newart and Ayenne comes to mind in that regard and it raises a lot of these 'mindset' issues that anybody who's looking into using any kind of OR/M solution should definitiely consider very carefully. OR/M definitely couples database and object model much more closely.

Scott
October 02, 2007

# re: A simple Business Object wrapper for LINQ to SQL

Rick:

You are correct - no leaks. Sorry for the confusion - I was misunderstanding what I saw in Reflector in how the connections were managed and had a problem in my code.

Jose Luis
October 06, 2007

# re: A simple Business Object wrapper for LINQ to SQL

Hello,

With Linq, do you Know how could I post two or more queries on an only trip to server?

Thanks

Rick Strahl
October 06, 2007

# re: A simple Business Object wrapper for LINQ to SQL

Jose - I don't think you can. LINQ will execute the query you've defined when it enumerates and that's that.

You can however, use Execute Query to create string based queries to execute and you can potentially return more than one resultset with that and assign with Translate.

Brady
January 25, 2008

# re: A simple Business Object wrapper for LINQ to SQL

Thanks! Good stuff.

Christian
February 07, 2008

# re: A simple Business Object wrapper for LINQ to SQL

Great work. I will take a deeper look at this.

I really like your blog, thank you very much!

Pierre
February 13, 2008

# re: A simple Business Object wrapper for LINQ to SQL

Great code!

I try to convert it to vb and add it on my projet. I got no compiler error, but when I try to create a new Business Entity i got a error in the Initialize function of the BusinessObject. I got a error on this line:

    Protected Overridable Sub Initialize()
        ' *** Create a default context 
        If Me.Context Is Nothing Then
            If Not String.IsNullOrEmpty(Me.Options.ConnectionString) Then  '***** HERRE 
                Me.CreateContext(Me.Options.ConnectionString)
            Else
                Me.Context = Me.CreateContext()
            End If
        End If

        ' *** Initialize Table Info 
        Me.TableInfo = New TableInfo(Context, GetType(TEntity))
    End Sub


when I try to watch the options value I got this error:
{ " The generic type ' BusinessFramework.wwBusinessObject`2 ' was used with an incorrect number of generic arguments in the assembly ' BusinessFramework, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null '. ": " BusinessFramework.wwBusinessObject`2 "}

any suggestion? Someone have aleready translate this framework in vb?
Thanks a lot

Pierre
February 14, 2008

# re: A simple Business Object wrapper for LINQ to SQL

OK I found the problem. The C# to VB translator forgot to recreate correctly the property like Options, Context, ... I recreate it and all seen to work.

In you sample application, you demonstrate how to create new, update and save entity. What about collection of entity like all the project binded to a grid. Do your conver the result in datatable or dataset? How do you keep traking of witch row are added, removed or changed by the grid and then persiste change to DB?

Thanks

Pierre
February 14, 2008

# re: A simple Business Object wrapper for LINQ to SQL

To ass some info to my last post.

for example I bind to my grid like this:

RadGridSegments.DataSource = oSegment.Converter.ToDataTable(oSegment.GetSegments, "Segments")

on the grid event I gat the changed row like this:

ctype(RadGridSegments.DataSource,DataTable).GetChanges()

Do I need to do a for each row in ctype(RadGridSegments.DataSource,DataTable).GetChanges()

and load with oSegement.load(pk) each modified line change the content and submitchange ? Same thing for deleted and added row? Do you have a more efficent way to do this?

Pierre
February 14, 2008

# re: A simple Business Object wrapper for LINQ to SQL

Me again,
Your code is very great! I start using it in my new project. Thanks to your framework!

For fun I try you disconnect mode, all work good for new entity, but if I try to load a entity with a PK, change something and call the .Save() function I got this error:

ex = {"An attempt has been made to Attach or Add an entity that is not new, perhaps having been loaded from another DataContext. This is not supported."}

If I trace the code, effectively a new datacontext are created at each request. This is normal in your code, any suggestion to remove this error? This is my simple test path:

            Dim oSeg2 As New busSegment
            oSeg2.Options.TrackingMode = TrackingModes.Disconnected
            oSeg2.Options.ConflictResolutionMode = ConflictResolutionModes.WriteNonConflictChanges
            oSeg2.Load(1)
            oSeg2.Entity.Nom = "Hello"
            oSeg2.Validate()
            oSeg2.Save()

Stephen Bridgett
March 09, 2008

# Unresolved compile error

Hi Rick

I have been studying the LINQ landscape for a long time and your solution to the need for a clear separation of DAL and BLL looks to be the right answer for LINQ as it stands at this time.

I grabbed the code but have a couple of unresolved errors and I wonder if I might pick your brain.

In the BusinessFramework table does not support either add or remove.

I am using VS2008 and Framework 3.5.

Perhaps there have been some late breaking changes in LINQ or perhaps I am simply overlooking something.

Thanks Rick

353 table.Add(entity);
523 table.Remove(entity);

Rick Strahl
March 09, 2008

# re: A simple Business Object wrapper for LINQ to SQL

Yes there were changes in LINQ for RTM. The code in the text here is slightly off because of these changes.

The download code from the link however should have the latest version that works correctly with RTM...

Stephen Bridgett
March 10, 2008

# Unresolved compile error

Hi Rick

I have been studying the LINQ landscape for a long time and your solution to the need for a clear separation of DAL and BLL looks to be the right answer for LINQ as it stands at this time.

I grabbed the code but have a couple of unresolved errors and I wonder if I might pick your brain.

In the BusinessFramework table does not support either add or remove.

I am using VS2008 and Framework 3.5.

Perhaps there have been some late breaking changes in LINQ or perhaps I am simply overlooking something.

Thanks Rick

353 table.Add(entity);
523 table.Remove(entity);


Hey thanks for the quick come back Rick.
The compile error is in the code from the link "Grab the code" so not sure what to make of this.
Suggestions?
Thanks

Rick Strahl
March 10, 2008

# re: A simple Business Object wrapper for LINQ to SQL

Ok, I see what's happening. The code is out of date. I've updated the link.

The correct link for .NET 3.5 RTM is:
http://www.west-wind.com/files/conferences/conn_LinqToSqlBusiness.zip

Fabien Ruffin
March 23, 2008

# re: A simple Business Object wrapper for LINQ to SQL

Hi Rick,

Your article seems to be a very solid approach to work with link. However, I'm still concerned about one thing. For you saving method to work it is required to have a timestamp field in each table in the database to figure out whether we're dealing with a new entity or not. Wouldn't it be simpler to check if the primary key is set or not on the entity? Because if an entity is new, its primary key sould be null until it's saved to the database.

I haven't had the time to try to implement it yet and I don't even know if such a thing would be easy to do or not... let me know what you think.

Thanks again for this great post.

Fab

James Hancock
March 24, 2008

# re: A simple Business Object wrapper for LINQ to SQL

Here's an idea that I've been playing with with Linq to SQL:

Why not manually make your classes since they're just a standard class with iNotify etc. on it.

Then add ot hte link context for the tables.

By doing so, you can write one class that handles all of your business logic AND in the process have full business logic implimented on every single property (i.e. throw errors intelligently etc. too)

And for data binding you can impliment the iErrorProvider etc. and then you get error reporting too.

Seems like a pretty elligant solution, and if you create a base class with inheritence you get everything you need all in one place. So what if you lose the designer? It's lame anyhow.

Rick Strahl
March 24, 2008

# re: A simple Business Object wrapper for LINQ to SQL

@John - For one thing the entities aren't your business objects - they're just data objects. If you add logic to them you're changing the whole dynamic of what an entity

Not saying that this can't work but you're now mixing DAL and busines layer into one.

James Hancock
March 25, 2008

# re: A simple Business Object wrapper for LINQ to SQL

To me, the DA doesn't need to be a layer. What needs to happen is a simple Business Layer with a "Save" command that returns an error if business rules aren't followed, but can be easily queried ad-hoc from anywhere with a simple syntax that makes sense (which Linq does and it can obviscate the end user from the database)

I'm not sure why you'd want a DAL separate from the Business Objects. To me the two go hand in hand and Linq to SQL works nicely because it gives us an excellent query environment that generates intelligent database queries, AND it can also do saves and we can intercept that (hopefully) and put in special logic for those saves.

The one thing that really screws this up is that you can't define any fields in your parent class that are on every table (really dumb design decision if you ask me!)

Ervin Marguc
March 25, 2008

# re: A simple Business Object wrapper for LINQ to SQL

Hi

I have use your simple business object in my new Winform application and I must say It is vary good idea to combine TEntity and TContext in business object and then write in all smart business logic and data manipulation. Class diagram is also very good, clear and understandable. I also have many issue to resolve how can I easily connect this new bussines object with my edit form which contain BindingSource, BindingNavigator, GridView and many field value editors but mostly issue are related to my "don't know how". Anyway I have some questions:

1. Is there any beter way to write some generic "Nullable check" in bussines rules, because we have allready in database model (dbml) for each collumn in table property "Nullable" set?
2. We also have define all FK references so is there some generic way to check reference integrity before delete event? btw. on MSDN I have found how to direct check one reference on detail table and then delete detail records in cascade mode but I don't wont to dump my hole database, I just wont to nicely inform user about reference problem...
3. What you suggest about best connection between Binding source and Business object? For this I mean best "synhronize mode", for example I use right now:
<code lang="c#">
public partial class frmEditUsers : Form
{
private DTUser BOUser = BulldySystemBusinessObjectFactory.GetUser();
private void frmEditUsers _Load(object sender, EventArgs e)
{
object qUser = BOUser.GetUserList();
this.DTUserBindingSource.DataSource = BOUser.Converter.ToList(qUser);
}
private void tolInsert_Click(object sender, EventArgs e)
{
DT_USER NewUser = BOUser.NewEntity();
this.DTUserBindingSource.Add(NewUser); // This code insert new Entity but BindingSource don't move to new Entity position, maybe I must manual set position?
}

private void tolDelete_Click(object sender, EventArgs e)
{
if (this.DTUserBindingSource.Position > -1)
{
if (MessageBox.Show("Are you sure to delete this record?", "Question", MessageBoxButtons.YesNo) == System.Windows.Forms.DialogResult.Yes)
{
DT_USER user = (DT_USER)this.DTUserBindingSource.Current;
if (!this.BOUser.Delete(user))
GetMainForm().StatusText = "Couldn't delete: " + this.BOUser.ErrorMessage;
else
{
// Record has been successfully deleted from database but not from DataBinging list!
// this.DTUserBindingSource.Remove(user); This code remove entitiy from BindingSource list but it also cause in some cases exception in BOUser.Save() -> Object reference not set to a instance of object
GetMainForm().StatusText = "Record has been successfully deleted.";
}
}
}
}

private void tolSave_Click(object sender, EventArgs e)
{
this.Validate();
this.DTUserBindingSource.EndEdit();
try
{
if (!BOUser.Save()) // This also throw exception if we just edit some value thru edit box, don't know how to save only changed records...
GetMainForm().StatusText = "Error while saving: " + this.BOUser.ErrorMessage;
}
catch (Exception ex)
{
GetMainForm().StatusText = ex.Message;
}
}
...
}
</code&gt.

On form I have all controls and grid view databind to DataBinding source and it have define datasource type of my Entity (DT_USER).
I must admit that I'am little confuse here, special because I come from Borland Delphi and BDE which don't have this problems but also don't have LINQ :)

Thank you very much for your time and any posible response you give me!

Ervin Marguc
March 25, 2008

# re: A simple Business Object wrapper for LINQ to SQL

huh, sorry for this massy code.

I also have correct some errors in code. In form Load event I have now: this.DTUserBindingSource.DataSource = qUser;

which bether works with BindingSource. I have also correct exception "Object reference not set to a instance of object", this error cause my code in override Validate() method...

Still I wish to make better delete code and of course some generic validate for foreign keys and nullable checks...

# http://www.onlinemarketingmaverick.com

« The earlier the better Paying for Serenity» Home Links Joshua Gans\' Web Site CoRE Research Game Theorist (Blog) Melb Business School IPRIA Econ Theory Centre Current interesting links Books Core Economics for Managers Finishing the Job Parentonomics Principles of Economics Publishing Economics Aust Econ Blogs Andrew Leigh Club Troppo John Quiggin Kalimna Peter Martin Blogs 26econ Freakonomics Greg Mankiw Marginal Revolution News for Econ Students The Dilbert Blog Tim Harford Greatest Hits An Inscrutable...

David Williams
April 01, 2008

# re: A simple Business Object wrapper for LINQ to SQL

I am getting errors with the VersionMember Property of the wwBuisnessObject class. Where is VersionMember pulled from, and what can be done to assure that VersionMember is not null?

Exception:Test method ProjectTest.busCategoryTest.GetCategoriesTest threw exception: System.ApplicationException: dbo.tblCategories doesn't have a version field. Business object tables mapped require a version field.

This is coming from wwBuisnessObject.cs:
public TableInfo(DataContext context, Type entityType)
 {
     if (metaTable.RowType.VersionMember == null)
         throw new ApplicationException(this.Tablename + " doesn't have a version field. Business object tables mapped require a version field.");
 }

// *** Generically get the table
 Table<TEntity> table = context.GetTable( typeof(TEntity)) as Table<TEntity>;

 // *** BIG assumption here: version field and using Reflection - yuk!
 //     can't see another way to figure out whether we're dealing with 
 //     a new entity or not.
 object tstamp = entity.GetType().GetProperty(this.TableInfo.VersionField).GetValue(entity, null); 

Steve
April 03, 2008

# re: A simple Business Object wrapper for LINQ to SQL

Your business object needs to have a timestamp for that table in your database.

ie. if you have a Customer object that is using tblCustomer table. Add a field that is a timestamp to the tblCustomer table

Adrian Grigore
April 20, 2008

# re: A simple Business Object wrapper for LINQ to SQL

I tried using the framework, but it seems I am doing something wrong when declaring the business object type.

I tried declaring my business object by inheriting from wwBusinessObject with the entity and the datacontext generated by the object relational designer as generic parameters. However, since TContext has to be a wwDataContext and DataContext cannot be instantiated without parameters, this does not seem to work. I get the following compilation error:

"The type 'DataClassesDataContext' cannot be used as type parameter 'TContext' in the generic type or method 'Westwind.BusinessFramework.wwBusinessObject<TEntity,TContext>'. There is no implicit reference conversion from 'DataClassesDataContext' to 'Westwind.BusinessFramework.wwDataContext'."

Do I have to derive my own datacontext class from wwDataContext? I tried finding out more about this by looking at your Timetrakker site, but it seems you are using the ORD datacontext without any further modification. I only have Visual Web Developer at the moment though, so I could not compile your project to see if it works.

Any help would be greatly appreciated.

Thanks,

Adrian

Rick Strahl
April 21, 2008

# re: A simple Business Object wrapper for LINQ to SQL

You need to inherit your data context from wwDataContext. You can do this in the model designer, by setting the base class for the DataContext.

The business object depends on various extension features I added to the data context, so this is not an optional step...

Adrian Grigore
April 21, 2008

# re: A simple Business Object wrapper for LINQ to SQL

Thanks, Rick! I di not even know there is a base class property.

Adrian

Shafeeq
May 12, 2008

# re: A simple Business Object wrapper for LINQ to SQL

Hi Rick,
First Thanks for your great articles..

I have similar question to the one Pierre asked, how do you handle collections of entities? for example collections bound to datagridview.. how do you handle inserts, updates and deletes and sending back the changes to the database?

Also, what is the preferred way of transferring collections of entities between methods in a single form or passing collections between different forms.

The second question is regarding the converter class, is there a way to convert a dataset / Datatable back to entities?

Thanks..

Rick Strahl
May 12, 2008

# re: A simple Business Object wrapper for LINQ to SQL

@Shafeeq - Personally I always do manual binding for inserts and updates. Actually, I rarely use grids for data entry - it's just wrong <g>. I tend to use pop up forms with AJAX for editing operations when they are small or separate forms.

If you use a business object the business object holds the entities and sub entities generally. You end up passing a single object that's a container of most everything else related to your business object. But that really depends on your scenario. Either way collections are just another object that can be passed around like individual entities.

Re: converter back to DataSet - no. Why would you want to do this? Either use entities or use data sets but not both. exporting to DataTables can be useful in some binding scenarios as DTs bind much faster while still giving support for paging/sorting etc. Also works with entities but it's MUCH slower due to Reflection overhead.

manit
May 19, 2008

# re: A simple Business Object wrapper for LINQ to SQL

nice work! been trying to do this for months!

Justin
May 29, 2008

# re: A simple Business Object wrapper for LINQ to SQL

I was wondering if you could use sqlmetal to build the dbml file. The problem I am seeing is that for some reason you cannot specify a base class for the DataContext with sqlmetal. I find this real strange because you cannot set the entity base class within the designer but you can set the datacontext base class. And with sql metal you can set the entity base class but not the datacontext. They should both be usable with both methods.

Shtoni
July 16, 2008

# re: A simple Business Object wrapper for LINQ to SQL

Thank you very much!

Beni
July 27, 2008

# re: A simple Business Object wrapper for LINQ to SQL

Thanks for this very nice wrapper. Very useful!

A little question I have:
Is the DataContext base in general automaticly thread safe?

So before I used the LINQ library, I've worked with a static DAL, so there is automaticly a thread safe Data Access.

But I'm not sure, how does the DataContext managed this. So mabe I can use the singleton pattern.....

Thanks and best regards
Beni from Switzerland, (sorry for my english :-)

Rick Strahl
July 27, 2008

# re: A simple Business Object wrapper for LINQ to SQL

The DataContext is NOT thread safe, so you'll need to manage that instance yourself. For more info on ways that you can manage that check out this later blog post:

Linq to SQL DataContext Lifetime Management
http://www.west-wind.com/weblog/posts/246222.aspx

Beni
July 27, 2008

# re: A simple Business Object wrapper for LINQ to SQL

Thanks for answer my question and the link!

best regards
Beni

Beni
July 28, 2008

# re: A simple Business Object wrapper for LINQ to SQL

Hi again :)

The last time, I've spend a lot of time, to understand all the classes etc... but I'm not be able to get the "DataContext" (for ASP.NET).

I've try to work with you're "DataContextFactory", but the problem I've go is the "ObjectDisposedException".

So the code from the "DataContextFactory.GetWebRequestScopedDataContextInternal(...)" give me not a null reference but an object how was already disposed.

How can I test, whether the "DataContext" was already disposed?


Thank!
Beni

Colin
August 06, 2008

# re: A simple Business Object wrapper for LINQ to SQL

Hi Rick

Correct me if I'm wrong, but wouldn't your idea of storing a datacontext with each business object mean that the business objects are not serializable? I'm working on a design for a new web project and was hoping to use your method to encapsulate my business objects, but I was also hoping to be able to do many things with them that require them to be serializable (e.g. put them into ViewState/Session, pass them to Webservices, store them as dependency properties in workflows etc). All of this is impossible if the business objects themselves aren't serializable to XML.

If I'm right, do you have any suggestions as to how to use Linq to SQL in an environment that requires serializable business objects?

Thanks

Col

Stephen Flynn
September 30, 2008

# Small Addition to the wwBusinessObject - LoadShallowCopy

The following code is a small addition to the wwBusinessObject.

Occasionally I need to copy objects to a new object and wanted a quick and easy way of doing it. Please feel free to point out any code that doesn't look right. This is my first post of any code back to a web page.

-----------------------------------------

public TEntity LoadShallowCopy(object primaryKey)
{

    string sql = "select * from " + this.TableInfo.Tablename + " where " + this.TableInfo.PkField + "={0}";
    this.SetError();

    try
    {
        TContext context = this.Context;

        // *** If disconnected we'll create a new context
        if (this.Options.TrackingMode == TrackingModes.Disconnected)
            context = this.CreateContext();

        IEnumerable<TEntity> EntityList = context.ExecuteQuery<TEntity>(sql, primaryKey);
        TEntity OriginalEntity = null;
        OriginalEntity = EntityList.Single();
        
        TEntity NewEntity = this.NewEntity();

        System.Collections.ObjectModel.ReadOnlyCollection<MetaDataMember> TableMembers = context.Mapping.GetTable(typeof(TEntity)).RowType.DataMembers;

        Type EntityType = typeof(TEntity);

        foreach (MetaDataMember TableMember in TableMembers)
        {
            if (TableMember.DbType != null)
            {
                System.Reflection.PropertyInfo PInfo = EntityType.GetProperty(TableMember.Name);
                if (PInfo != null)
                {
                    PInfo.SetValue(NewEntity, PInfo.GetValue(OriginalEntity, null), null);
                }
            }
        }

        // *** Clear the primary key and the version field
        EntityType.GetProperty(this.TableInfo.PkField).SetValue(NewEntity, null, null);
        EntityType.GetProperty(this.TableInfo.VersionField).SetValue(NewEntity, null, null);

        // *** and return instance
        return NewEntity;
    }
    catch (InvalidOperationException)
    {
        // *** Handles errors where an invalid Id was passed, but SQL is valid
        this.SetError("Couldn't load original entity - invalid key provided.");
        this.Entity = this.NewEntity();
        return null;
    }
    catch (Exception ex)
    {
        // *** handles Sql errors
        this.SetError(ex);
        this.Entity = this.NewEntity();
    }

    return null;
}

lakmal molligoda
January 16, 2009

# re: A simple Business Object wrapper for LINQ to SQL

i hope following code will be use full for any one who needs to convert list into dataset. and thanks rick for the great piece of code.
public DataSet ConvertTo<T>(IList<T> list)
        {
            DataTable table = CreateTable<T>();
            Type entityType = typeof(T);
            PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(entityType);

            foreach (T item in list)
            {
                DataRow row = table.NewRow();

                foreach (PropertyDescriptor prop in properties)
                {
                    row[prop.Name] = prop.GetValue(item);
                }

                table.Rows.Add(row);
            }

            DataSet dSet = new DataSet();
            dSet.Tables.Add(table);

            return dSet;
        }

        
        public DataTable CreateTable<T>()
        {
            Type entityType = typeof(T);
            DataTable table = new DataTable(entityType.Name);
            PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(entityType);

            foreach (PropertyDescriptor prop in properties)
            {
                if (prop.PropertyType.Name == "Nullable`1")
                {
                    table.Columns.Add(prop.Name,System.Nullable.GetUnderlyingType(prop.PropertyType));
                }
                else
                {
                    table.Columns.Add(prop.Name, prop.PropertyType);
                }
            }

            return table;
        }

mk
March 24, 2009

# re: A simple Business Object wrapper for LINQ to SQL

Another interesting project in this area (at least for me):
http://www.codeplex.com/MultiTierLinqToSql

(mk)

Gus
May 15, 2009

# re: A simple Business Object wrapper for LINQ to SQL

Hi Rick,

While implementing your business objects framework I noticed that you used VersionMember. My Qestion concerns neccesity. In the scenerio that I use, I was to use SQLServer with the default security objects that are normally in aspnetdb. Specifically, I want to embed their controls in my database which would allow me to not have create login code. I also want to use the user objects as a level in a n-way tree representing the application. I don't want to create my own user mechanism like you have in your TimeTrakker app. The aspnet tables root table is aspnet_Applications which doesn't have a timestamp. This is the primary partition for aspnet security. I want to add to this table at will. Your business are throwing this application exception. I want to know the consequences of removing this exception since I don't want to modify the base security tables.

Gus
May 15, 2009

# re: A simple Business Object wrapper for LINQ to SQL

Hi Rick,

While implementing your business objects framework and I noticed that you use the variable VersionMember. My Qestion concerns neccesity. In my scenario , I want to use SQLServer with the default security objects that are normally reside in aspnetdb. Specifically, I want to embed their controls in my database. This would give me the luxury of not having to create the login code. I also want to use the user objects as a level in a n-way tree representing the application. I don't want to create my own user mechanism like you have in your TimeTrakker app. The aspnet tables root table is aspnet_Applications which doesn't have a timestamp. This is the primary partition for aspnet security. I want to add to this table at will. Your business objects are throwing an application exception. I want to know the consequences of removing this exception since I don't want to modify the base security tables?

Note: Sorry for my previous incoherent message !!

Rick Strahl
May 15, 2009

# re: A simple Business Object wrapper for LINQ to SQL

@Gus - The code posted here requires the version field - it uses it to figure out if an entity is new or updated. The original version expected that, but I've since updated this slightly so that it works without the version and instead attempting to check a Pk field which is somewhat less efficient.

The more current version of the BusinessObjectWrapper is available in the West Wind Web Toolkit project here:

Web Toolkit:
http://www.west-wind.com/WestwindWebToolkit/

Docs:
http://www.west-wind.com/WestwindWebToolkit/docs?page=_2mt0581bd.htm

Subversion:
http://www.west-wind.com:8080/svn/WestwindWebToolkit/trunk/Westwind.BusinessFramework/LinqToSql/

Hope this helps,

+++ Rick ---

Paul
August 12, 2009

# re: A simple Business Object wrapper for LINQ to SQL

Hi Rick,

First off thanks alot for this - it's great!

I have a quick question on the subject of the query type methods - I personally don't like to instantiate an instance of the object just to call a method on it (if it doesnt needed to be initialized) like this...

BusContact bc = new BusContact();
bc.FindContacts(contactSearchParams);

If those helper type methods were static then we could say
BusContact.FindContacts(contactSearchParams)

so BusContact could act more like a service (for those sorts of helper methods).

The method would either have to make a new DataContext or use a static one, but for these query type of methods, are there still issues with a static DataContext?

What I mean is, would it be kosher for the base class to declare a static dataContext called QueryDataContext or something, that the static query type methods could take advantage of?

Is passing something back as Iqueryable that comes from a static dataContext an issue?

Just wondering, thanks again!

Rick Strahl
August 13, 2009

# re: A simple Business Object wrapper for LINQ to SQL

You don't want those objects to be static especially not in Web applications. The point is that you need to separate the data contexts for different requests otherwise you have cross talk.

Basically the bus object follows the same unit of work pattern that the DataContext on its own would follow and that ensures that connections are shut down and the context clears out between requests.

IAC, I wouldn't recommend static objects or for that matter caching the DataContext.

Jamie
December 19, 2009

# re: A simple Business Object wrapper for LINQ to SQL

Stuggling to understand some of the C# syntax and convert it to VB.

For:
    public class wwBusinessObject<TEntity,TContext>
           where TEntity: class, new()
           where TContext: wwDataContext, new()
 
...
 
this.TableInfo = new TableInfo(Context, typeof(TEntity)); 


I Have:

Public Class BaseClass(Of TEntity As {Class, New})
...
Me.TableInfo = New TableInfo(db, TypeOf (TEntity))


however it is telling me TEntity is a type and cannot be used as an expression.

I guess
where TEntity: class, new()

creates an instance of TEntity? Do you know how I achieve this in VB?

Thanks

Jason Jones
December 30, 2009

# re: A simple Business Object wrapper for LINQ to SQL

I'm new and remain confused by your answer to Scott's question dated Sept 27, 2007:

"@Scott - Each business object has its own data context, not each entity. If you are dealing with say an Entry entity in the example, the Entity has access to all related entities and if
necessary in the business layer code can access the context to load others. '

So does this mean that f I have a "Salesperson" business object, for example, that is exposed via a WPF application which permits multiple data entry forms to be open, then I would (or should) have one and only one data context instance shared by all of salesperson instances, since they're the same type? Or would I have a separate data context for each and every business object instance, even if they are of the same type?

I've looked at all the code on the SVN repository and am impressed and grateful for your contribution, but also a bit overwhelmed to be honest. Is there a simple "task-list" example program or something with source code that i could study to see how to actually implement this in a real world setting? Apologies if it's in there already, but I couldn't find it. Warm Regards and thank you, jbj

G
March 17, 2010

# re: A simple Business Object wrapper for LINQ to SQL

Hi,
I was wondering about using stored procedures with your approach. Do you already do this or do you not bother with stored procedures in favor of using LinqToSql queries? If you do use stored procedures, can you explain your approach?
Thanks,
G

Rick Strahl
March 17, 2010

# re: A simple Business Object wrapper for LINQ to SQL

@G - Linq to SQL handles stored procedures as method imports on a static object and you can use those imports directly. The behavior of those stored procedure results are the same as returned lists or entities run with explicit LINQ statements, so there's no special handling required.

Draak
August 03, 2010

# re: A simple Business Object wrapper for LINQ to SQL

Hi Rick,
I've been reading a fair bit recently about L2S in the context of N-Tier applications. What I've found is that there some folks and M$ who would disagree with your characterization of L2S entities as merely data transfer objects. Zain Naboulsi writing on MSDN blog http://blogs.msdn.com/b/zainnab/archive/2008/08/20/dr-strangelinq-or-how-i-learned-to-stop-worrying-and-love-no-data-access-layer.aspx, says plainly that L2S entities are both DTO's and business objects. He also recommends placing BL methods directly in the DataContext. Also, the following article on MSDN, if I am interpreting the chart correctly, places validation logic and business rules inside the partial entity classes. Your thoughts?

Rick Strahl
August 03, 2010

# re: A simple Business Object wrapper for LINQ to SQL

@Draak, I don't agree with either of those statements, but to be clear that's a matter of preference. There's a lot of ways to skin the cat of transferring data over multiple tiers and validation in the business layer. Placing BL logic directly on entities is a bad choice because you can't inherit common functionality so you're constantly rewriting the same code and end up using pure L2S syntax for all data access operations. That stuff belongs into a business layer IMHO not on entities or else you will repeat the same code over and over and over again. Assuming you already go down the path of a separate business layer it makes good sense to also isolate validation logic. To me keeping the entities clean and have something one level above the entities manipulate the core data access makes much more sense because often multiple entities have to interact with each other and that happens anyway at a higher level. If you put code at the entity level you then also end up with code at some higher level and your business logic gets scattered into multiple places which is... terrible :-). Much more flexibility with higher level logic IMHO.

Eripsni
October 14, 2010

# re: A simple Business Object wrapper for LINQ to SQL

Hi Rick,

Lets say I have a bunch of Invoices, and all invoices have a bunch of invoice-lines (details).
In order to delete an invoice i need to delete the invoice-lines before deleting the invoice.
how will this be done most efficiently with this Business Framework wrapper classes?

Chandra
December 06, 2010

# re: A simple Business Object wrapper for LINQ to SQL

Hi Rick - I really like your linq-to-sql Business object wrapper utility that i have used in some of my personal projects. I wonder could the same utility be used for Linq-Entity framework based Data access projects with little/no changes at all? Please throw your suggestions.

Thanks
Chandra

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024