21

I wrote the following method.

public T GetByID(int id)
{
    var dbcontext = DB;
    var table = dbcontext.GetTable<T>();
    return table.ToList().SingleOrDefault(e => Convert.ToInt16(e.GetType().GetProperties().First().GetValue(e, null)) == id);
}

Basically it's a method in a Generic class where T is a class in a DataContext.

The method gets the table from the type of T (GetTable) and checks for the first property (always being the ID) to the inputted parameter.

The problem with this is I had to convert the table of elements to a list first to execute a GetType on the property, but this is not very convenient because all the elements of the table have to be enumerated and converted to a List.

How can I refactor this method to avoid a ToList on the whole table?

[Update]

The reason I can't execute the Where directly on the table is because I receive this exception:

Method 'System.Reflection.PropertyInfo[] GetProperties()' has no supported translation to SQL.

Because GetProperties can't be translated to SQL.

[Update]

Some people have suggested using an interface for T, but the problem is that the T parameter will be a class that is auto generated in [DataContextName].designer.cs, and thus I cannot make it implement an interface (and it's not feasible implementing the interfaces for all these "database classes" of LINQ; and also, the file will be regenerated once I add new tables to the DataContext, thus loosing all the written data).

So, there has to be a better way to do this...

[Update]

I have now implemented my code like Neil Williams' suggestion, but I'm still having problems. Here are excerpts of the code:

Interface:

public interface IHasID
{
    int ID { get; set; }
}

DataContext [View Code]:

namespace MusicRepo_DataContext
{
    partial class Artist : IHasID
    {
        public int ID
        {
            get { return ArtistID; }
            set { throw new System.NotImplementedException(); }
        }
    }
}

Generic Method:

public class DBAccess<T> where T :  class, IHasID,new()
{
    public T GetByID(int id)
    {
        var dbcontext = DB;
        var table = dbcontext.GetTable<T>();

        return table.SingleOrDefault(e => e.ID.Equals(id));
    }
}

The exception is being thrown on this line: return table.SingleOrDefault(e => e.ID.Equals(id)); and the exception is:

System.NotSupportedException: The member 'MusicRepo_DataContext.IHasID.ID' has no supported translation to SQL.

[Update] Solution:

With the help of Denis Troller's posted answer and the link to the post at the Code Rant blog, I finally managed to find a solution:

public static PropertyInfo GetPrimaryKey(this Type entityType)
{
    foreach (PropertyInfo property in entityType.GetProperties())
    {
        ColumnAttribute[] attributes = (ColumnAttribute[])property.GetCustomAttributes(typeof(ColumnAttribute), true);
        if (attributes.Length == 1)
        {
            ColumnAttribute columnAttribute = attributes[0];
            if (columnAttribute.IsPrimaryKey)
            {
                if (property.PropertyType != typeof(int))
                {
                    throw new ApplicationException(string.Format("Primary key, '{0}', of type '{1}' is not int",
                                property.Name, entityType));
                }
                return property;
            }
        }
    }
    throw new ApplicationException(string.Format("No primary key defined for type {0}", entityType.Name));
}

public T GetByID(int id)
{
    var dbcontext = DB;

    var itemParameter = Expression.Parameter(typeof (T), "item");
    var whereExpression = Expression.Lambda<Func<T, bool>>
        (
        Expression.Equal(
            Expression.Property(
                 itemParameter,
                 typeof (T).GetPrimaryKey().Name
                 ),
            Expression.Constant(id)
            ),
        new[] {itemParameter}
        );
    return dbcontext.GetTable<T>().Where(whereExpression).Single();
}
Community
  • 1
  • 1
Andreas Grech
  • 105,982
  • 98
  • 297
  • 360
  • You don't need to worry about the designer generated files or the edmx designer over writing them.. you don't implement the interface in the designer file.. you'll write a partial class for the entities that implements the interface. – meandmycode Apr 09 '09 at 19:12
  • But that means that I should make every "db class" implement this interface, no ? – Andreas Grech Apr 09 '09 at 19:14
  • Yes it would, but it's a one time piece of work and then your code will be much more robust. – Neil Williams Apr 09 '09 at 19:18
  • The GetPrimaryKey method is a bit dodgy, linq to sql doesn't always use attributes to explain the mapping, you can use entirely dbml.. whatever you use however will be the same in the Mappings definition example which Denis Troller gave. – meandmycode Apr 09 '09 at 21:39
  • Yes, better use the Mapping as I did, because it will work whatever way you map (attributes or XML files). Also, it should be quite a bit faster (reflection is slow). In any case, you should really cache the result of GetPrimaryKey() for performance. – Denis Troller Apr 09 '09 at 21:42
  • Sorry for taking you down the wrong path bro. I can't vote myself down so I need someone to do it for me. – Restore the Data Dumps Apr 09 '09 at 22:14
  • Oh no problem man, we all learned from this eh ;-) thanks for your input mate. – Andreas Grech Apr 09 '09 at 22:23

6 Answers6

18

What you need is to build an expression tree that LINQ to SQL can understand. Assuming your "id" property is always named "id":

public virtual T GetById<T>(short id)
{
    var itemParameter = Expression.Parameter(typeof(T), "item");
    var whereExpression = Expression.Lambda<Func<T, bool>>
        (
        Expression.Equal(
            Expression.Property(
                itemParameter,
                "id"
                ),
            Expression.Constant(id)
            ),
        new[] { itemParameter }
        );
    var table = DB.GetTable<T>();
    return table.Where(whereExpression).Single();
}

This should do the trick. It was shamelessly borrowed from this blog. This is basically what LINQ to SQL does when you write a query like

var Q = from t in Context.GetTable<T)()
        where t.id == id
        select t;

You just do the work for LTS because the compiler cannot create that for you, since nothing can enforce that T has an "id" property, and you cannot map an arbitrary "id" property from an interface to the database.

==== UPDATE ====

OK, here's a simple implementation for finding the primary key name, assuming there is only one (not a composite primary key), and assuming all is well type-wise (that is, your primary key is compatible with the "short" type you use in the GetById function):

public virtual T GetById<T>(short id)
{
    var itemParameter = Expression.Parameter(typeof(T), "item");
    var whereExpression = Expression.Lambda<Func<T, bool>>
        (
        Expression.Equal(
            Expression.Property(
                itemParameter,
                GetPrimaryKeyName<T>()
                ),
            Expression.Constant(id)
            ),
        new[] { itemParameter }
        );
    var table = DB.GetTable<T>();
    return table.Where(whereExpression).Single();
}


public string GetPrimaryKeyName<T>()
{
    var type = Mapping.GetMetaType(typeof(T));

    var PK = (from m in type.DataMembers
              where m.IsPrimaryKey
              select m).Single();
    return PK.Name;
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Denis Troller
  • 7,411
  • 1
  • 23
  • 36
  • And what is the solution to overcome different field names? – Andreas Grech Apr 09 '09 at 20:56
  • as in automatically, not specifying them in an abstract parameter from the sub class or anything that requires further maintenance – Andreas Grech Apr 09 '09 at 20:57
  • you could try to extract it from the DataContext's MappingSource I think. Let me see... – Denis Troller Apr 09 '09 at 21:15
  • Found the solution! I'm using his GetPrimaryKey() extension method he has in his MVc open source website. Will post the solution in a bit – Andreas Grech Apr 09 '09 at 21:22
  • I did not see it in his post, so I got rid of it. Here's a possible implementation. – Denis Troller Apr 09 '09 at 21:32
  • also, you really should build some caching around that, so that you don't go around querying for the PK each time you call, but that is left as an exercise to the reader (or is left out for the sake of brevity, peek your favorite disclaimer) – Denis Troller Apr 09 '09 at 21:39
  • I download the Suket.Shop (mvs website) he has and searched in his files for that GetPrimaryKey function heh. – Andreas Grech Apr 09 '09 at 21:39
  • ...and now I have quite a lot of work to do to fully understanding his methods, as regards Expression Trees and Funcs because I haven't yet really delved deeped inside those. – Andreas Grech Apr 09 '09 at 21:40
  • For info, until .NET 4.0 ships, there are very good reasons (with LINQ-to-SQL) to use Single(pred) rather than Where(pred).Single() - it avoids a round-trip to the server for primary key fetches (where it has already seen the object). – Marc Gravell Aug 08 '09 at 19:28
  • Thanks for the info. My VB habits show, I very rarely use the method form of queries because it is so much easier in query form in VB :) – Denis Troller Aug 10 '09 at 08:34
1

What if you rework this to use GetTable().Where(...), and put your filtering there?

That would be more efficient, since the Where extension method should take care of your filtering better than fetching the entire table into a list.

Reed Copsey
  • 554,122
  • 78
  • 1,158
  • 1,373
1

Some thoughts...

Just remove the ToList() call, SingleOrDefault works with an IEnumerably which I presume table is.

Cache the call to e.GetType().GetProperties().First() to get the PropertyInfo returned.

Cant you just add a constraint to T that would force them to implement an interface that exposes the Id property?

sisve
  • 19,501
  • 3
  • 53
  • 95
0

Ok, check this demo implementation. Is attempt to get generic GetById with datacontext(Linq To Sql). Also compatible with multi key property.

using System;
using System.Data.Linq;
using System.Data.Linq.Mapping;
using System.Linq;
using System.Reflection;
using System.Collections.Generic;

public static class Programm
{
    public const string ConnectionString = @"Data Source=localhost\SQLEXPRESS;Initial Catalog=TestDb2;Persist Security Info=True;integrated Security=True";

    static void Main()
    {
        using (var dc = new DataContextDom(ConnectionString))
        {
            if (dc.DatabaseExists())
                dc.DeleteDatabase();
            dc.CreateDatabase();
            dc.GetTable<DataHelperDb1>().InsertOnSubmit(new DataHelperDb1() { Name = "DataHelperDb1Desc1", Id = 1 });
            dc.GetTable<DataHelperDb2>().InsertOnSubmit(new DataHelperDb2() { Name = "DataHelperDb2Desc1", Key1 = "A", Key2 = "1" });
            dc.SubmitChanges();

            Console.WriteLine("Name:" + GetByID(dc.GetTable<DataHelperDb1>(), 1).Name);
            Console.WriteLine("");
            Console.WriteLine("");
            Console.WriteLine("Name:" + GetByID(dc.GetTable<DataHelperDb2>(), new PkClass { Key1 = "A", Key2 = "1" }).Name);
        }
    }

    //Datacontext definition
    [Database(Name = "TestDb2")]
    public class DataContextDom : DataContext
    {
        public DataContextDom(string connStr) : base(connStr) { }
        public Table<DataHelperDb1> DataHelperDb1;
        public Table<DataHelperDb2> DataHelperD2;
    }

    [Table(Name = "DataHelperDb1")]
    public class DataHelperDb1 : Entity<DataHelperDb1, int>
    {
        [Column(IsPrimaryKey = true)]
        public int Id { get; set; }
        [Column]
        public string Name { get; set; }
    }

    public class PkClass
    {
        public string Key1 { get; set; }
        public string Key2 { get; set; }
    }
    [Table(Name = "DataHelperDb2")]
    public class DataHelperDb2 : Entity<DataHelperDb2, PkClass>
    {
        [Column(IsPrimaryKey = true)]
        public string Key1 { get; set; }
        [Column(IsPrimaryKey = true)]
        public string Key2 { get; set; }
        [Column]
        public string Name { get; set; }
    }

    public class Entity<TEntity, TKey> where TEntity : new()
    {
        public static TEntity SearchObjInstance(TKey key)
        {
            var res = new TEntity();
            var targhetPropertyInfos = GetPrimaryKey<TEntity>().ToList();
            if (targhetPropertyInfos.Count == 1)
            {
                targhetPropertyInfos.First().SetValue(res, key, null);
            }
            else if (targhetPropertyInfos.Count > 1) 
            {
                var sourcePropertyInfos = key.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);
                foreach (var sourcePi in sourcePropertyInfos)
                {
                    var destinationPi = targhetPropertyInfos.FirstOrDefault(x => x.Name == sourcePi.Name);
                    if (destinationPi == null || sourcePi.PropertyType != destinationPi.PropertyType)
                        continue;

                    object value = sourcePi.GetValue(key, null);
                    destinationPi.SetValue(res, value, null);
                }
            }
            return res;
        }
    }

    public static IEnumerable<PropertyInfo> GetPrimaryKey<T>()
    {
        foreach (var info in typeof(T).GetProperties().ToList())
        {
            if (info.GetCustomAttributes(false)
            .Where(x => x.GetType() == typeof(ColumnAttribute))
            .Where(x => ((ColumnAttribute)x).IsPrimaryKey)
            .Any())
                yield return info;
        }
    }
    //Move in repository pattern
    public static TEntity GetByID<TEntity, TKey>(Table<TEntity> source, TKey id) where TEntity : Entity<TEntity, TKey>, new()
    {
        var searchObj = Entity<TEntity, TKey>.SearchObjInstance(id);
        Console.WriteLine(source.Where(e => e.Equals(searchObj)).ToString());
        return source.Single(e => e.Equals(searchObj));
    }
}

Result:

SELECT [t0].[Id], [t0].[Name]
FROM [DataHelperDb1] AS [t0]
WHERE [t0].[Id] = @p0

Name:DataHelperDb1Desc1


SELECT [t0].[Key1], [t0].[Key2], [t0].[Name]
FROM [DataHelperDb2] AS [t0]
WHERE ([t0].[Key1] = @p0) AND ([t0].[Key2] = @p1)

Name:DataHelperDb2Desc1
Simone S.
  • 1,756
  • 17
  • 18
0

Maybe executing a query might be a good idea.

public static T GetByID(int id)
    {
        Type type = typeof(T);
        //get table name
        var att = type.GetCustomAttributes(typeof(TableAttribute), false).FirstOrDefault();
        string tablename = att == null ? "" : ((TableAttribute)att).Name;
        //make a query
        if (string.IsNullOrEmpty(tablename))
            return null;
        else
        {
            string query = string.Format("Select * from {0} where {1} = {2}", new object[] { tablename, "ID", id });

            //and execute
            return dbcontext.ExecuteQuery<T>(query).FirstOrDefault();
        }
    }
Misha N.
  • 3,455
  • 1
  • 28
  • 36
0

Regarding:

System.NotSupportedException: The member 'MusicRepo_DataContext.IHasID.ID' has no supported translation to SQL.

The simple workaround to your initial problem is to specify an Expression. See below, it works like a charm for me.

public interface IHasID
{
    int ID { get; set; }
}
DataContext [View Code]:

namespace MusicRepo_DataContext
{
    partial class Artist : IHasID
    {
        [Column(Name = "ArtistID", Expression = "ArtistID")]
        public int ID
        {
            get { return ArtistID; }
            set { throw new System.NotImplementedException(); }
        }
    }
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131