Monday, June 02, 2008
Because this has bitten me in the ass more than once.
When using ActiveRecord to query the database the following error occurs:
Failed: Castle.ActiveRecord.Framework.ActiveRecordException : Could not perform SlicedFindAll for ShoppingCart
----> NHibernate.QueryException : could not resolve property: UserId of: MyDomain.Core.ShoppingCart
Problem: The Property values in the criteria are case sensitive.
Where the query looks like this:
public ShoppingCart LoadCurrentBasket(string userId, Guid customerId)
{
DetachedCriteria criteria = DetachedCriteria.For<ShoppingCart>();
criteria.Add(Expression.Eq("UserId", userId));
criteria.Add(Expression.Eq("CustomerId", customerId));
return ActiveRecordMediator<ShoppingCart>.FindFirst(criteria);
}
And the class looks like this:
[ActiveRecord(Table = "Basket")]
public class ShoppingCart
{
private IList<ShoppingCartItem> _LineItems = new List<ShoppingCartItem>();
[PrimaryKey(PrimaryKeyType.Guid)]
public Guid BasketID { get; set; }
[Property("CustomerId")]
public Guid CustomerID { get; set;}
[Property("UserID")]
public string UserID { get; set;}
///
///
///
}
Thursday, January 17, 2008
In a hand coded data-access layer you often end up with some variant of this pattern when translating a datareader to an entity.
public List<ICarrier> LoadList()
{
List<ICarrier> items = new List<ICarrier>();
using (SafeDataReader reader = new AdoHelper().ExecuteDataReader(CommandType.Text, "Select * from _lu_Carrier"))
{
while (reader.Read())
{
items.Add(MapCarrier(reader));
}
}
return items;
}
where your ADOHelper does whatever it needs to do to connect to the database and run your query. This is generally coupled with "mapping" method (MapCarrier in this instance)
public static ICarrier MapCarrier(SafeDataReader dr)
{
ICarrier carrier = new Carrier();
carrier.Name = dr.GetString("Name");
carrier.Phone = dr.GetString("Phone");
carrier.SCAC = dr.GetString("SCAC");
carrier.URL = dr.GetString("URL");
return carrier;
}
I think this is a fairly standard approach, and it works well. However, it's tedious and can lead to lots of repetition as your DAL grows. Enter the magic of generics and delegates which allow the code in the first block to become something like this:
public List<ICarrier> GetList()
{
return new AdoHelper().ExecuteList<ICarrier>(CommandType.Text,
"Select * from _lu_Carrier",
MapCarrier);
}
ExecuteList<T> is overloaded to allow for a few variations which all route to this:
public List<T> ExecuteList<T>(CommandType commandType, string commandText, Func<T, SafeDataReader> mappingCallback, params SqlParameter[] parameters)
{
List<T> collection = new List<T>();
using (SafeDataReader reader = ExecuteDataReader(commandType, commandText, parameters))
{
while (reader.Read())
{
collection.Add(mappingCallback(reader));
}
}
return collection;
}
The "magic" here is the Func<T SafeDataReader> delegate being passed in. This is (lifted directly from Rhino.Commons) and is declared as
public delegate TResult Func<TResult, S>(S s1);
ExecuteItem<T> is similar, but returns a single entity instead of a collection
public T ExecuteItem<T>(CommandType commandType, string commandText, Func<T, SafeDataReader> mappingCallback, params SqlParameter[] parameters)
{
using (SafeDataReader reader = ExecuteDataReader(commandType, commandText, parameters))
{
while (reader.Read())
{
return mappingCallback(reader);
}
}
return default(T);
}
Over even a small project this can save a lot of typing and a substantial amount of time.
Thursday, January 10, 2008
File under stupid semantic tricks.
Extension methods (which seem ripe for abuse and all kinds of bad code) provide some great flexibility when working with fluent interfaces. For example, take the following extensions to the date class
public static class ExtensionMethods
{
public static bool IsBefore(this DateTime dt, DateTime other)
{
return other > dt;
}
}
This allows us to better express our intent
public class WorkingWith
{
public void Test1()
{
DateTime myBirthday = new DateTime(2008, 3, 1);
DateTime christmans = new DateTime(2008, 12,25);
if (myBirthday.IsBefore(christmans))
{
Console.WriteLine("yes");
}
}
}
Which is nice, but can get better by wiring in a call to a utility class
public static class ExtensionMethods
{
...
public static IsBetweenDateRange IsBetween(this DateTime dt, DateTime lowDate)
{
return new IsBetweenDateRange(dt, lowDate);
}
}
which allows a readable (as opposed to parse-then-translate-able) statement such as
public voidTest2()
{
DateTime myBirthday = new DateTime(2008, 3, 1);
DateTime halloween = new DateTime(2008, 10, 31);
DateTime christmans = new DateTime(2008, 12,25);
if (halloween.IsBetween(myBirthday).And(christmans))
{
}
}
The utility class in this case being
public class IsBetweenDateRange
{
private DateTime _baseDate;
private DateTime _lowDate;
public IsBetweenDateRange(DateTime baseDate, DateTime lowDate)
{
_baseDate = baseDate;
_lowDate = lowDate;
}
public bool And(DateTime highDate)
{
return _baseDate > _lowDate && _baseDate < highDate;
}
}
Tuesday, January 08, 2008
This tutorial covers using Binsor2 in an asp.net application.
Because of different versions and the dearth of information available about getting started with Binsor, I would recommend getting the files listed later in this article directly from the trunk as the syntax between versions 1 and 2 has changed. There is a readme file covering how to build the trunk, and I would add the following points to that:
1. Run your "Visual Studio Command Line" so that MSBuild is in your path (Start|Programs|Microsoft Visual Studio 200x|Visual Studio Tools)
2. I had to add a folder called "lib" to "C:\Program Files\Microsoft SDKs\Windows\v6.0A" to get the build to run.
Getting Started
Create a new library called BinsorDemo.Domain, and add the following domain entities (fields made public for brevity).
public class Account
{
public string FirstName;
public string LastName;
public string State;
public decimal Balance;
public List<Purchase> PurchaseHistory;
}
public class Purchase
{
public DateTime Date;
public decimal Amount;
public decimal AmountDue;
public Purchase(DateTime date, decimal amount, decimal amountDue)
{
Date = date;
Amount = amount;
AmountDue = amountDue;
}
}
Next, add the following interfaces. These are the services that we will "wire-up" using Binsor.
public interface ICreditService
{
decimal GetCreditRemaining(Account c);
}
public interface IInterestCalculator
{
decimal CalculateInterestOn(Account account);
}
Finally, add the following concrete implementations of the interfaces.
public class CreditService : ICreditService
{
private decimal _CreditLimit;
public CreditService(decimal creditLimit)
{
_CreditLimit = creditLimit;
}
public decimal GetCreditRemaining(Account account)
{
decimal totalOutstanding = 0M;
account.PurchaseHistory.ForEach(delegate (Purchase p){totalOutstanding += p.Amount - p.AmountDue;});
return _CreditLimit = totalOutstanding;
}
}
public class InterestCalculator : IInterestCalculator
{
private readonly decimal _InStateAmount;
private readonly decimal _OutOfStateAmount;
private readonly string _ThisState;
public InterestCalculator(decimal inStateAmount, decimal outOfStateAmount, string thisState)
{
_InStateAmount = inStateAmount;
_OutOfStateAmount = outOfStateAmount;
_ThisState = thisState;
}
private decimal DetermineInterestRateFor(Account account)
{
if (account.State.Equals(_ThisState, StringComparison.InvariantCultureIgnoreCase))
return _InStateAmount;
else
return _OutOfStateAmount;
}
public decimal CalculateInterestOn(Account account)
{
decimal interestRate = DetermineInterestRateFor(account);
decimal interest = 0M;
account.PurchaseHistory.ForEach(delegate (Purchase purchase){interest += purchase.AmountDue*interestRate;});
return interest;
}
}
Create the Web Application
Next create a web application called BinsorDemo.Web. Add the following references (as mentioned above, I would pull these directly from the trunk - these will be located in [x:\trunk]\rhino-commons\Rhino.Commons\bin\Debug).
Boo.Lang
Boo.Lang.Compiler
Boo.Lang.Parser
Castle.Core
Castle.Windsor
log4net
Rhino.Commons
BnisorDemo.Domain
Add a new file to the root of the application called "windsor.boo", and add the following to it.
import BinsorDemo.Domain
creditLimitAmount as decimal = 500.0
component 'credit_service', ICreditService, CreditService:
creditLimit=creditLimitAmount
inState as decimal = .05
outOfState as decimal = .075
myState = 'MD'
component 'interest_service', IInterestCalculator, InterestCalculator:
inStateAmount = inState
outOfStateAmount = outOfState
thisState = "MD"
Next add a global code file (global.asax), if it is not already there. Add the following to this file. The Application_Start event is where the magic takes place.
public class Global : System.Web.HttpApplication
{
private static IWindsorContainer container;
public static IWindsorContainer Container
{
get { return container; }
}
protected void Application_Start(object sender, EventArgs e)
{
XmlConfigurator.Configure();
string binsorConfigFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "windsor.boo");
container = new WindsorContainer();
BooReader.Read(container, binsorConfigFilePath);
}
}
Finally
Let's run a quick test to see if everything is working OK. Add the following to a WebForm's Page_Load event, and navigate to that page.
protected void Page_Load(object sender, EventArgs e)
{
Account demoAccount = new Account();
demoAccount.FirstName = "Foo";
demoAccount.LastName = "Bar";
demoAccount.Balance = 250;
demoAccount.State = "MD";
demoAccount.PurchaseHistory = new List<Purchase>();
demoAccount.PurchaseHistory.Add(new Purchase(new DateTime(2007, 10, 16), 100M, 15M));
demoAccount.PurchaseHistory.Add(new Purchase(new DateTime(2007, 11, 16), 10M, 0));
demoAccount.PurchaseHistory.Add(new Purchase(new DateTime(2007, 12, 16), 250M, 35M));
Response.Write(Global.Container.Resolve<ICreditService>().GetCreditRemaining(demoAccount));
Response.Write("<hr/>");
Response.Write(Global.Container.Resolve<IInterestCalculator>().CalculateInterestOn(demoAccount));
}
Thursday, December 13, 2007
Thursday, November 15, 2007
On HP laptops with the built-in credential manager (fingerprint scanner) running with VMWare (server) running, lsass.exe will crash while logging in telling you that your computer will reboot in 60 seconds. As documented here, there are a couple of workarounds for this
- Disable the fingerprint scanner
- Set the VMWare services to Manual instead of Automatic (start | run | services.msc)
Because it's almost 2010 and I want Minority Report style eyeball scanning technology, I settled for number 2 and set following services to Manual. (they sounded important, and had a circular dependency on each other)
- VMWare Authorization Service [VMAuthdService]
- VMWare Registration Service [VMServerdWin32, RPC Service]
I left the other 3 services alone as they seemed innocent enough, and had no dependencies. They were
- VMWare DHCP Service [VMNetDHCP]
- VMWare NAT Service [VMWare NAT Service]
- VMWare Virtual Mount Manager Extended [vmount2]
Obviously, the services I stopped need to be running for the VMWare application to run, and they do not start without being told to (I was hoping that launching the application would trigger the loading of these). After verifying that I could start and stop these services from the command line, I added a batch file named VMWareServices to my "Startup" folder with the following commands:
net start vmauthdservice
net start vmserverdwin32
AND IT STILL FAILED
Next I tried reversing the order these were called in. This stopped the crash, but for whatever reason vmserverdwin32 did not start.
I then added a ping delay delay of 60 seconds. Still no good. I could get to the command prompt and type the net start command, and I received the message that the service didn't start in a timely fashion. Immediately try again and it fires up. Same results as 120 and 180 seconds.
@#$*&($*!#^)&%^*^#$^@
Out of frustration more than anything else, I set the services back to the original order with a 120 second delay and it worked. Knocked it down to 60 seconds, and it's still good.
SOLUTION
It's irritating, and it's far from optimal, and I'm not sure why I need the delay, but it does the job.
ping 127.0.0.1 -n 60
net start vmauthdservice
net start vmserverdwin32
Wednesday, October 31, 2007
In Working Effectively with Legacy Code Michael Feathers describes legacy code as "code without tests". I describe it as the crap I wrote 4 years ago.
The code block that I was working with looked like the image to the left. It was a single method that was 278 lines long, had a cyclomatic complexity of 80 and (according to DevExpress' DXCore) a horrific maintenance complexity of 1766. The method was used to validate warranty claims and had conditional branching for "Save", "Get Price Details", and "Place". In addition, the "Get Price Details" and "Place" blocks had conditional statements to handle cases for specific countries.
The first step in refactoring this to something resembling a maintainable state was to extract the method in to it's own WarrantyValidator class. This meant creating a constructor that accepted all of the variables and fields from the original object that would be required to do the validation. In this case I needed the Warranty Claim being validated, the user's country, a list to store the failure messages in (a discussion for another day) and the number of items on the claim.
The end result is a class that looked like this (this is "blog" code)
public class ValidateClaim
{
private readonly WarrantyClaim ClaimToValidate;
private int NumberOfItemsOnClaim;
private readonly string CountryCode;
private readonly List<string> ErrorCodes;
public ValidateClaim(WarrantyClaim claimToValidate, int numberOfItemsOnClaim, string countryCode, List<string> errorCodes)
{
ClaimToValidate = claimToValidate;
NumberOfItemsOnClaim = numberOfItemsOnClaim;
CountryCode = countryCode;
ErrorCodes = errorCodes;
}
public bool Validate(string ActionName)
{
if (ActionName == "SAVE")
{
//save-specific validation for example
if (string.IsNullOrEmpty(ClaimToValidate.AuthorizationNumber))
{
ErrorCodes.Add("12345678-1234-1234-123456789012");
}
}
else if (ActionName == "PRICE")
{
//pricing-specifi validation
if (CountryCode == "US")
{
//us specific validation
}
else if (CountryCode == "GB")
{
//gb specific valudation
}
}
else if (ActionName == "PLACE")
{
if (CountryCode == "US")
{
//us specific validation
}
else if (CountryCode == "GB")
{
//gb specific valudation
}
}
/*
* lots of validation no matter what the action is
* ...
* ...
* ...
*/
return ErrorCodes.Count == 0;
}
}
The next step (which I guess probably should have been the first) was generate tests to cover as much code as possible. In doing this I found that I needed a couple of additional parameters in my constructor as data that was exposed through the properties of the WarrantyClaim wasn't available without banging against the database. At the end of this phase, I had managed to get about 95% test coverage, and some analysis of the WarrantyClaim suggested that a couple of cases for which validation code existed were no longer possible. With 76 "green" tests I began breaking up the object and re-running the tests for verification after every change.
The first change was to create the WarrantyValidatorBase. This abstract class contained a constructor identical to my WarrantyValidator class and an abstract method called Validate.
public abstract class WarrantyValidatorBase
{
protected readonly WarrantyClaim ClaimToValidate;
protected int NumberOfItemsOnClaim;
protected readonly string CountryCode;
protected readonly List<string> ErrorCodes;
public WarrantyValidatorBase(WarrantyClaim claimToValidate, int numberOfItemsOnClaim, string countryCode, List<string> errorCodes)
{
ClaimToValidate = claimToValidate;
NumberOfItemsOnClaim = numberOfItemsOnClaim;
CountryCode = countryCode;
ErrorCodes = errorCodes;
}
public abstract void Validate();
}
From here I moved all of the Save validation to it's own class, and repeated this with the Price Details and Place validation. Next, I extracted the country specific validation into separate classes - WarrantyPlaceValidationUS, WarrantyPlaceValidationGB, etc.
public class WarrantyPlaceValidator : WarrantyValidatorBase
{
public WarrantyPlaceValidator(WarrantyClaimWarrantyClaim claimToValidate, int numberOfItemsOnClaim,
string countryCode, List<string> errorCodes)
: base(claimToValidate, numberOfItemsOnClaim, countryCode, errorCodes)
{
}
public override void Validate()
{
//place specific validation
if (CountryCode == "US")
{
new WarrantyPlaceValidatorUS(ClaimToValidate, NumberOfItemsOnClaim, CountryCode, ErrorCodes).Validate();
}
else if (CountryCode == "GB")
{
//gb specific valudation
new WarrantyPlaceValidatorGB(ClaimToValidate, NumberOfItemsOnClaim, CountryCode, ErrorCodes).Validate();
}
}
}
public class WarrantyPlaceValidatorUS : WarrantyValidatorBase
{
public WarrantyPlaceValidatorUS(WarrantyClaim claimToValidate, int numberOfItemsOnClaim,
string countryCode, List<string> errorCodes)
: base(claimToValidate, numberOfItemsOnClaim, countryCode, errorCodes)
{
}
public override void Validate()
{
//us specific validation
}
}
public class WarrantyPlaceValidatorGB : WarrantyValidatorBase
{
public WarrantyPlaceValidatorGB(WarrantyClaimclaimToValidate, int numberOfItemsOnClaim,
string countryCode, List<string> errorCodes)
: base(claimToValidate, numberOfItemsOnClaim, countryCode, errorCodes)
{
}
public override void Validate()
{
//GB specific validation
}
}
After this refactoring, my original diagram looked a little like this:
Again, after each change, I re-ran my tests and made sure that everything was still ok.
So far, so good. The new code was a far easier to maintain than the previous approach. I now had small testable classes, which were easy to read and instantly understandable. They all had very specific purposes and were very loosely coupled. However, there was still an awful lot of duplication in the constructors, and I was not happy with the country specific cases being handled this way.
I knew this needed to be resolved, but I was a little stuck. I am pretty clueless when it comes to design patterns and to be perfectly honest, the GoF book makes me want to gouge my eyes out. Fortunately, the Head First book and Alex Henderson's Windsor tutorial gave me a little insight into the Decorator pattern. Time for some more refactoring...
The WarrantyValidatorBase ended up looking like this.
public abstract class WarrantyValidatorBase
{
private List<string> _ErrorPhrases;
private WarrantyClaim _WarrantyClaim;
private WarrantyValidatorBase _ChildValidator;
private int _NumberOfPartsInBasket;
public WarrantyValidatorBase()
{
}
public WarrantyValidatorBase(WarrantyValidatorBase decorateWith)
{
_ChildValidator = decorateWith;
}
protected void AddErrorId(string errorGuid)
{
_ErrorPhrases.Add(errorGuid);
}
internal ToolCommerce.Warranty WarrantyClaim
{
get { return _WarrantyClaim; }
set { _WarrantyClaim = value; }
}
internal List<string> Phrases
{
get { return _ErrorPhrases; }
set { _ErrorPhrases = value; }
}
internal int NumberOfPartsInBasket
{
get { return _NumberOfPartsInBasket; }
set { _NumberOfPartsInBasket = value; }
}
internal WarrantyValidatorBase ChildValidator
{
get { return _ChildValidator; }
set { _ChildValidator = value; }
}
public abstract void Validate();
protected virtual void ValidateChild()
{
if (_ChildValidator!=null)
{
_ChildValidator.Phrases = _ErrorPhrases;
_ChildValidator.WarrantyClaim = _WarrantyClaim;
_ChildValidator.NumberOfPartsInBasket = _NumberOfPartsInBasket;
_ChildValidator.Validate();
}
}
}
Note the addition of the _ChildValidator field and the constructor changes to accept another object deriving from WarrantyValidatorBase. I also moved the arguments from the constructor to properties - this just seemed a little cleaner to me. The ValidateChild method is key here. It passes the validation request down the "decorator chain" to its ChildValidator, and its ChildValidator to its ChildValidator and so on. This approach allowed me to change my WarrantyPlaceValidator to this:
public class WarrantyPlaceValidator : WarrantyValidatorBase
{
public WarrantyPlaceValidator(WarrantyValidatorBase childValidation)
{
ChildValidator = childValidation;
}
public override void Validate()
{
//place specific validation
//notice that there is nothing specific to countries in here any more
ValidateChild();
}
}
End Result
The awesomeness of the decorator is that I can chain these together and add functionality without having to clutter up my classes with conditionals and branching logic.
public void ValidateForPlace(Warranty claimToValidate, int numberOfItemsOnClaim, string countryCode, List<string> errorCodes)
{
WarrantyValidatorBase validator = null;
if (countryCode == "US")
{
validator = new WarrantyValidator(new WarrantyPlaceValidator(new WarrantyPlaceValidatorUS()));
}
else if (countryCode == "GB")
{
validator = new WarrantyValidator(new WarrantyPlaceValidator(new WarrantyPlaceValidatorGB()));
}
else
{
//no country specific validation
validator = new WarrantyValidator(new WarrantyPlaceValidator());
}
validator.NumberOfPartsInBasket = numberOfItemsOnClaim;
validator.Phrases = errorCodes;
validator.WarrantyClaim = claimToValidate;
validator.Validate();
}
If new functionality is required, it can be encapsulated and then "chained" in.
Monday, October 15, 2007
In case I run into this again...
During a build for a .net solution using Team Foundation Server, we got an error that said.
error MSB3041: Unable to create a manifest resource name for "NewService\WarrantyUpload.aspx.resx". Could not find a part of the path 'E:\Builds\My App\Source\Code\MyApp.WebApp\Folder\File.aspx.vb'.
What had happened was that \Folder had been accidentally dragged and dropped into another folder.
Thursday, September 20, 2007
Where this is code that is duplicated all over the place with only minor alterations for the data source and the default value:
DropDownList cbo = new DropDownList();
cbo.DataSource = PersonMotherObject.GetPersons();
cbo.DataTextField = "FirstName";
cbo.DataValueField = "LastCommaFirst";
cbo.DataBind();
cbo.Items.Insert(0, new ListItem("", "----- SELECT -----"));
foreach (ListItem listItem in cbo.Items)
{
listItem.Selected = listItem.Value == "Flanders, Ned";
}
This reads a lot easier, and gets us a little more DRY:
new ComboBoxSetter<Person>()
.WithListControl(ddl)
.WithDataSourceOf(PersonMotherObject.GetPersons())
.SetSelectedWhereTextIs("Ned")
.AddLeadingItemOf("", "-----Select------")
.TheDataTextFieldIs("FirstName")
.TheDataValueFieldIs("LastCommaFirst")
.Bind();
The class that does the grunt work (reformatted for brevity)
/// <summary>
/// Provides a fluent interface for setting a combo box
/// </summary>
public class ComboBoxSetter<T>
{
private ListControl Control;
private IList<T> DataSource;
private string DefaultValue;
private ListItem ListItemToAdd;
private string DataValueField;
private string DataTextField;
public void Bind()
{
Control.DataSource = DataSource;
Control.DataTextField = DataTextField;
Control.DataValueField = DataValueField;
Control.DataBind();
if (ListItemToAdd != null)
Control.Items.Insert(0, ListItemToAdd);
if (!string.IsNullOrEmpty(DefaultValue))
foreach (ListItem listItem in Control.Items)
listItem.Selected = listItem.Text == DefaultValue;
}
public ComboBoxSetter<T> WithListControl(ListControl ddl)
{
Control = ddl;
return this;
}
public ComboBoxSetter<T> WithDataSourceOf(IList<T> people)
{
DataSource = people;
return this;
}
public ComboBoxSetter<T> SetSelectedWhereTextIs(string defaultSelection)
{
DefaultValue = defaultSelection;
return this;
}
public ComboBoxSetter<T> AddLeadingItemOf(string value, string display)
{
ListItemToAdd = new ListItem(display, value);
return this;
}
public ComboBoxSetter<T> AddLeadingItemOf(ListItem leadingItem)
{
ListItemToAdd = leadingItem;
return this;
}
public ComboBoxSetter<T> TheDataValueFieldIs(string dataValueField)
{
DataValueField = dataValueField;
return this;
}
public ComboBoxSetter<T> TheDataTextFieldIs(string dataTextField)
{
DataTextField = dataTextField;
return this;
}
Thursday, September 13, 2007
After writing the same 20 lines of boilerplate code for the 1000th time, I decided to encapsulate it in a class and add some syntactic sweetness to it in the form of a fluent interface as recently demonstrated by Joey Beninghove.
What I had been doing was something like the following pseudocode (or a slight variation) in every place I wanted to cache something.
declare returnObject as null
if the HTTPCache available
if the object is available in the cache
set returnObject
else
set returnObject to whatever code is needed to get the object
add returnObject to the cache
else
set returnObject to whatever code is needed to get the object
return returnObject
Here is the class I ended up with.
public class HTTPCacheHelper<T>
{
private DateTime? _AbsoluteExpiration;
private TimeSpan? _SlidingExpiration;
private CacheItemPriority _Priority = CacheItemPriority.Default;
private string _Key;
/// <summary>
/// Key of item stored in the cache.
/// </summary>
public HTTPCacheHelper<T> WithKeyOf(string key)
{
_Key = key;
return this;
}
/// <summary>
/// Absolute date to expire on
/// </summary>
/// <remarks>
/// Optional, but either this or WithSlidingExpiration must be called
/// </remarks>
public HTTPCacheHelper<T> WithExpirationDateOf(DateTime expires)
{
_AbsoluteExpiration = expires;
return this;
}
/// <summary>
/// Sliding timespan to expire on.
/// </summary>
/// <remarks>
/// Optional, but either this or WithExpirationDateOf must be called
/// </remarks>
public HTTPCacheHelper<T> WithSlidingExpiration(TimeSpan ts)
{
_SlidingExpiration = ts;
return this;
}
/// <summary>
/// Priority
/// </summary>
public HTTPCacheHelper<T> WithPriorityOf(CacheItemPriority priority)
{
_Priority = priority;
return this;
}
/// <summary>
/// Adds the item to the cache based data passed in in previous methods.
/// This will the last method call in the chain, and must
/// be called in order to for the item to be cached.
/// </summary>
public void Commit(T item)
{
Debug.Assert(!item.Equals(default(T)));
Debug.Assert(!_AbsoluteExpiration.HasValue ^ _SlidingExpiration.HasValue);
Debug.Assert(!string.IsNullOrEmpty(_Key));
if (!_AbsoluteExpiration.HasValue) _AbsoluteExpiration = Cache.NoAbsoluteExpiration;
if (!_SlidingExpiration.HasValue) _SlidingExpiration = Cache.NoSlidingExpiration;
lock (HttpRuntime.Cache)
{
HttpRuntime.Cache.Add(_Key, item, null, _AbsoluteExpiration.Value, _SlidingExpiration.Value, _Priority, null);
}
}
public T GetItem(string key)
{
if (HttpRuntime.Cache != null)
{
object check = HttpRuntime.Cache[key];
if (check != null)
{
return (T)check;
}
}
return default(T);
}
private bool IsNull(T item)
{
if (typeof(T).IsValueType)
{
return default(T).Equals(item);
}
else
{
return item == null;
}
}
public T GetItem(string key, Func<T> itemSourceOnDoesNotExist)
{
T check = GetItem(key);
if (IsNull(check))
{
check = itemSourceOnDoesNotExist();
}
return check;
}
public T GetItem(string key, Func<T> itemSourceOnDoesNotExist, Proc<T> addToCache)
{
T check = GetItem(key);
if (IsNull(check))
{
check = itemSourceOnDoesNotExist();
addToCache(check);
}
return check;
}
}
The methods prior to Commit() make up the fluent interface, and I stole a little code from Ayende's Rhino Commons for the delegates in the GetItem methods. As a side note, the GetItems methods were originally static, which allowed me to do the following:
private string ItemToCache()
{
return "expected value";
}
[Test]
public void FluentSyntaxText()
{
HTTPCacheHelper<string> cache = new HTTPCacheHelper<string>();
cache
.WithExpirationDateOf(DateTime.Now.AddDays(1))
.WithKeyOf("TestKey")
.WithPriorityOf(CacheItemPriority.Low)
.Commit(ItemToCache());
string actualValue = HTTPCacheHelper<string>.GetItem("TestKey", ItemToCache, cache.Commit);
Assert.AreEqual("expected value", actualValue);
}
But it dawned on me a little later that by making them instance methods I could accomplish the same thing in a single statement
[Test]
public void FluentSyntaxTest2()
{
HTTPCacheHelper<string> cache = new HTTPCacheHelper<string>();
string actualValue = cache
.WithExpirationDateOf(DateTime.Now.AddDays(1))
.WithKeyOf("TestKey")
.WithPriorityOf(CacheItemPriority.Low)
.GetItem("TestKey", ItemToCache, cache.Commit);
Assert.AreEqual("expected value", actualValue);
}
Which is pretty friggin' sweet.