Chris Sainty

A technical blog covering full-stack web development.

twitter | github | stackoverflow

LINQ to SQL: Extending Data Classes

Next we are going to look at how you can use partial classes and partial methods to add functionality to your generated LINQ-to-SQL classes. One of the nice things about partial classes and the new partial methods is that you can extend the generated classes into a separate file that is not destroyed when you update the underlying data model. See my complaint here about other ways of changing the classes that does not have this benefit.
First things first is to create a new C# class file, I have called it AdventureWorks_Extra.cs to sort it below the AdventureWorks.dbml

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace AdventureWorks
{
    partial class Address
    {

    }
}

Here we will be extending the Address class, but you can extend any of the generated classes. If we type partial we are presented with a list of partial methods we can implement.

partial

Each property (or field in the database) gets its own Changed() and Changing() property and the class itself has OnCreated(), OnLoaded() and OnValidate(). So what exactly are partial methods and how do they differ from events or overwriting a base class method.

Here is a code snippet from AdventureWorks.designer.cs from the Address class to show the definition and calling of a partial method. I have cut out a bunch of other definitions to keep the snippet small.

public partial class Address : INotifyPropertyChanging, INotifyPropertyChanged
{
    partial void OnLoaded();
    partial void OnValidate(System.Data.Linq.ChangeAction action);
    partial void OnCreated();

    public Address()
    {
        OnCreated();
    }
}

What the compiler does when it sees a partial method is go looking for an implementation. Not one of the partial methods generated by the LINQ-to-SQL designer has any code associated with it by default, that is for you to do. If the compiler can not find an implementation it actually removes the definition and all the calls from the compiled class. This offers component designers in particular a way to offer thousands of binding points to their code, that will have absolutely no impact on performance or code size unless the developer adds some code, and this is exactly what the LINQ-to-SQL team have done. Its quite a nifty new feature of the language.

Going back to actually implementing some of these methods on our Address class. Lets add a partial method to the class that will uppercase our City property for us.

public partial class Address
{
    partial void OnCityChanged()
    {
        _City = _City.ToUpper();
    }
}

We can then check this from our code

AdventureWorksDataContext db = new AdventureWorksDataContext();
db.Log = Console.Out;

Address a = new Address();
a.City = "Seattle";
Console.WriteLine(a.City); // SEATTLE

Alternatively we can use the OnValidate() method to apply some business logic checks.

partial void OnValidate(System.Data.Linq.ChangeAction action)
{
    if (action == ChangeAction.Delete && CustomerAddresses.Count() != 0)
    {
        throw new Exception("Can not delete an address that is in use");
    }
}

AdventureWorksDataContext db = new AdventureWorksDataContext();
db.Log = Console.Out;

Address addr = db.Addresses.Where(a => a.CustomerAddresses.Count() > 0).First();
db.Addresses.DeleteOnSubmit(addr);
db.SubmitChanges(); // Throws Exception

Another use for this technique is to add helper functions to your underlying data object. For example we are going to add a method to our Address class that can format up an address label (suitable for an envelope)

public string GetAddressLabel()
{
    return AddressLine1 + "\n"
        + (AddressLine2 == null ? "" : AddressLine2 + "\n")
        + City + " " + PostalCode + " " + StateProvince.StateProvinceCode + "\n"
        + StateProvince.CountryRegion.Name;
}

AdventureWorksDataContext db = new AdventureWorksDataContext();
db.Log = Console.Out;

Address addr = db.Addresses.First();
Console.WriteLine(addr.GetAddressLabel());

// 1970 Napa Ct.
// Bothell 98011 WA
// United States

There is just a couple of options available to you for extending the generated data model. One area of LINQ I have not investigated thoroughly yet is adding functionality to LINQ itself to provide methods on IQueryable that can alter the expression tree that gets turned in SQL. If these sorts of changes are possible it will open all sorts of possibilities.