tags:

views:

111

answers:

2

I'm using a shim property to make sure that the date is always UTC. This in itself is pretty simple but now I want to query on the data. I don't want to expose the underlying property, instead I want queries to use the shim property. What I'm having trouble with is mapping the shim property. For example:

public partial class Activity
{
    public DateTime Started
    {
     // Started_ is defined in the DBML file
     get{ return Started_.ToUniversalTime(); }
     set{ Started_ = value.ToUniversalTime(); }
    }
}


var activities = from a in Repository.Of<Activity>()
        where a.Started > DateTime.UtcNow.AddHours( - 3 )
        select a;

Attempting to execute the query results in an exception:

System.NotSupportedException: The member 'Activity.Started' has no supported 
translation to SQL.

This makes sense - how could LINQ to SQL know how to treat the Started property - it's not a column or association? But, I was looking for something like a ColumnAliasAttribute that tells SQL to treat properties of Started as Started_ (with underscore).

Is there a way to help LINQ to SQL translate the expression tree to the Started property can be used just like the Started_ property?

+2  A: 

There's a code sample showing how to do that (i.e. use client-side properties in queries) on Damien Guard's blog:

http://damieng.com/blog/2009/06/24/client-side-properties-and-any-remote-linq-provider

That said, I don't think DateTime.ToUniversalTime will translate to SQL anyway so you may need to write some db-side logic for UTC translations anyway. In that case, it may be easier to expose the UTC date/time as a calculated column db-side and include in your L2S classes.

E.g.:

create table utc_test (utc_test_id int not null identity,
  local_time datetime not null,
  utc_offset_minutes int not null,
  utc_time as dateadd(minute, 0-utc_offset_minutes, local_time),
  constraint pk_utc_test primary key (utc_test_id));  

insert into utc_test (local_time, utc_offset_minutes) values ('2009-09-10 09:34', 420); 
insert into utc_test (local_time, utc_offset_minutes) values ('2009-09-09 22:34', -240);   

select * from utc_test
KristoferA - Huagati.com
This almost works. The problem is that it requires .WithTranslations everywhere for LINQ statements - even with using ...Auto statement. At that point it's no better than just referencing the Started_ property directly. I really want the fact that the property is shimmed to be transparent from the calling code.
Paul Alexander
If the db structure is not locked (i.e. belongs to someone else or some app that can't be changed), I'd recommend going for option #2 - either store as UTC in the database, or store as local+offset and calculate UTC in a computed column. That has the extra advantage of protecting you from doing ToUniversalTime on a local time from a different time zone than the time zone you're in...
KristoferA - Huagati.com
+1  A: 

Based on @KrstoferA's answer I came up with a reliable solution that hides the fact that the properties are aliased from client code. Since I'm using the repository pattern returning an IQueryable[T] for specific tables, I can simply wrap the IQueryable[T] result provided by the underlying data context and then translate the expression before the underlying provider compiles it.

Here's the code:

public class TranslationQueryWrapper<T> : IQueryable<T>
{
    private readonly IQueryable<T> _source;

    public TranslationQueryWrapper( IQueryable<T> source )
    {
        if( source == null ) throw new ArgumentNullException( "source" );
        _source = source;
    }

    // Basic composition, forwards to wrapped source.
    public Expression Expression { get { return _source.Expression; } }
    public Type ElementType { get { return _source.ElementType; } }
    public IEnumerator<T> GetEnumerator() { return _source.GetEnumerator(); }
    IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); }

    // Intercept calls to the provider so we can translate first.
    public IQueryProvider Provider 
    { 
        get { return new WrappedQueryProvider(_source.Provider); } 
    }

    // Another wrapper around the provider
    private class WrappedQueryProvider : IQueryProvider
    {
        private readonly IQueryProvider _provider;

        public WrappedQueryProvider( IQueryProvider provider ) { 
            _provider = provider; 
        }

        // More composition
        public object Execute( Expression expression ) { 
            return Execute( expression ); }
        public TResult Execute<TResult>( Expression expression ) { 
            return _provider.Execute<TResult>( expression ); }
        public IQueryable CreateQuery( Expression expression ) { 
            return CreateQuery( expression ); }

        // Magic happens here
        public IQueryable<TElement> CreateQuery<TElement>( 
            Expression expression ) 
        { 
            return _provider
                .CreateQuery<TElement>( 
                    ExpressiveExtensions.WithTranslations( expression ) ); 
        }
    }
}
Paul Alexander