Wednesday, January 05, 2011

Good to be Lazy<T>?

Just read Dino Esposito's article Don’t Worry, Be Lazy about the new new Lazy<T> class in .NET Framework 4.0. For short it's a wrapper class to simplify your work for the "lazy loading" approach.

Sometimes we don't want to populate all related data for a complex object at one shot. Ideally, for the performance consideration, we should only retrieve data that are needed. A class is usually abstracted from business model, for instance a Customer object would have properties of ID, Name, Address, Purchased Orders, etc. Populating all these fields could be costly and may require a few expensive calls to backend system. It's possible that only a small portion of an object data is used when requesting a Customer object. Why those purchased orders data are filled when only Customer's name is in interest? Lazy<T> is designed to help for such scenarios: delay loading data until they are actually requested.

Following code example demos such usage. A User class has an Address property, and the Address data are only loaded once when Address property is accessed:

class Program
{
class Address
{
public string Location { get; set; }
public string GeoCode { get; set; }
public Address(string loc, string geo)
{
this.Location = loc;
this.GeoCode = geo;
}
}

class User // User has Address property which is lazily loaded
{
public string ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }

public User(string id, string fName, string lName)
{
this.ID = id;
this.FirstName = fName;
this.LastName = lName;
_repoAddress = new Lazy<Address>(GetUserAddress);
}

private Lazy<Address> _repoAddress = null; // Lazy container loaded from repository
private Address _setAddress = null; // Container to setter
public Address Address
{
get
{
if (_setAddress == null) // Only get repository data if not being set
return _repoAddress.Value;
else
return _setAddress;
}
set
{
_setAddress = value;
}
}
private Address GetUserAddress() // Method to load the address data from repository
{
Console.WriteLine("...... Loading address for user {0} ......", this.ID);
return UserService.GetUserAddress(this.ID);
}
}

static List<Lazy<User>> _repoUserList = new List<Lazy<User>>(); // Fake User repository
static List<Lazy<Address>> _repoAddressList = new List<Lazy<Address>>(); // Fake Address repository
static void MockData() // Mock some sample data
{
for (int i = 1; i <= 10; i++)
{
string fakeID = i.ToString();
_repoUserList.Add(new Lazy<User>(() =>
{
Console.WriteLine("... Mocking User {0} ...", fakeID);
return new User(fakeID, "FName" + fakeID, "LName" + fakeID);
}));
_repoAddressList.Add(new Lazy<Address>(() =>
{
Console.WriteLine("... Mocking Address {0}...", fakeID);
return new Address("Location" + fakeID, "GeoCode" + fakeID);
}));
}
}

class UserService // Fake Service
{
public static Address GetUserAddress(string ID)
{
var addr = _repoAddressList.FirstOrDefault(a => a.Value.GeoCode == "GeoCode" + ID);
return (addr == null) ? null : new Address(addr.Value.Location, addr.Value.GeoCode);
}
public static User GetUserByID(string ID)
{
var user = _repoUserList.FirstOrDefault(u => u.Value.ID == ID);
return (user == null) ? null : new User(user.Value.ID, user.Value.FirstName, user.Value.LastName);
}
}

static void Main()
{
MockData();
User user = UserService.GetUserByID("5");
Console.WriteLine("The user's name: {0} {1}", user.FirstName, user.LastName);
Console.WriteLine("The user's address: {0} {1}", user.Address.Location, user.Address.GeoCode);
user.Address = new Address(user.Address.Location + "(new)", user.Address.GeoCode + "(new)");
Console.WriteLine("The user's updated address: {0} {1}", user.Address.Location, user.Address.GeoCode);
Console.Read();
}
}
Result:
... Mocking User 1 ...
... Mocking User 2 ...
... Mocking User 3 ...
... Mocking User 4 ...
... Mocking User 5 ...
The user's name: FName5 LName5
...... Loading address for user 5 ......
... Mocking Address 1 ...
... Mocking Address 2 ...
... Mocking Address 3 ...
... Mocking Address 4 ...
... Mocking Address 5 ...
The user's address: Location5 GeoCode5
The user's updated address: Location5(new) GeoCode5(new)
Above demo code searches a DAO User object from the mock data list, whose Address property is not loaded until its value gets accessed. Notice the private lazy<Address> property is only used in the getter method, and would be overridden by setter value so the custom value can be saved and passed back to service layer for further process.

A LINQ expression can be used to initiate the Lazy<T> as shown in the mock data population. We can see only the first 5 mocked Users were created because the fifth User was returned and the rest 5 mocked Users were not being accessed at all.

The last note is that all Lazy collections in above demo code are not thread-safe. To make Lazy<T> thread-safe, you need to pass a LazyExecutionMode parameter to its constructor, for detail refer to MSDN.