Goose
Goose

Reputation: 3289

Generic method where T implements Interface<T>

I'm trying to create a generic data retrieval process. What I have currently works, but there is a part of it that doesn't seem right and I'm hoping there is a better way to accomplish it.

So the idea is that I have classes for each table in the database, here is an example of a class:

public class CMCGRGRGROUP : IFacetsObject<CMCGRGRGROUP>
{
    public int GRGR_CK { get; set; }
    public string GRGR_NAME { get; set; }
    public string GRGR_ADDR1 { get; set; }

    public IEnumerable<CMCGRGRGROUP> ToObject(DataTable table)
    {
        return table.AsEnumerable().Select(row =>
        {
            return new CMCGRGRGROUP
            {
                GRGR_CK = Convert.ToInt32(row["GRGR_CK"]),
                GRGR_NAME = row["GRGR_NAME"].ToString(),
                GRGR_ADDR1 = row["GRGR_ADDR1"].ToString()
            };
        });
    }
}

You'll notice that the class implements an interface of its own type. The interface simply defines a method called ToObject, which is used to convert a datatable to a class of that particular type:

public interface IFacetsObject<T>
{
    IEnumerable<T> ToObject(DataTable obj);
}

Now, here is the method that I am using to execute a query:

public IEnumerable<T> ExecuteQuery<T>(string sql, IFacetsObject<T> obj) where T : new()
{
    using (var conn = new AseConnection(_conn))
    {
        conn.Open();
        var cmd = new AseCommand(sql, conn);

        var dt = new DataTable();
        var da = new AseDataAdapter(sql, conn);
        da.Fill(dt);                

        return obj.ToObject(dt); //this is the interface method
    }
}

So the main question is: How can the generic method know that T should implement IFacetsObject<T>? That way I don't have to pass IFacetsObject<T> as a parameter. Ideally, I could change the return line to be something like this:

return T.ToObject(dt);

And call it like this:

var result = ExecuteQuery<CMCGRGRGROUP>(sql).Take(5);

Instead of like this:

var result = ExecuteQuery<CMCGRGRGROUP>(sql, new CMCGRGRGROUP()).Take(5);

I'll admit that I'm not terribly familiar with generics yet so there may be something within the implementation that isn't right.

Upvotes: 15

Views: 22242

Answers (2)

Wolfwyrd
Wolfwyrd

Reputation: 15916

You can add a constraint on your ExecuteQuery method. You already have one: requiring that T be newable. You'd declare it like:

public IEnumerable<T> ExecuteQuery<T>(string sql, IFacetsObject<T> obj) 
  where T : IFacetsObject<T>, new()
{
    using (var conn = new AseConnection(_conn))
    {
        conn.Open();
        var cmd = new AseCommand(sql, conn);

        var dt = new DataTable();
        var da = new AseDataAdapter(sql, conn);
        da.Fill(dt);                

        return obj.ToObject(dt); //this is the interface method
    }
}

So it now knows T is an IFacetsObject<T>. You could now do:

public IEnumerable<T> ExecuteQuery<T>(string sql) 
  where T : IFacetsObject<T>, new()
{
    using (var conn = new AseConnection(_conn))
    {
        conn.Open();
        var cmd = new AseCommand(sql, conn);

        var dt = new DataTable();
        var da = new AseDataAdapter(sql, conn);
        da.Fill(dt);                

        return new T().ToObject(dt); //this is the interface method
    }
}

Which IMO is still pretty ugly.

EDIT Response:

Note that you cannot call T.ToObject - an interface cannot define a static method. The workaround is the use of new to create a new instance of T and call the instance method.

Upvotes: 8

recursive
recursive

Reputation: 86174

You need a generic constraint on your interface. Declare it like this:

public interface IFacetsObject<T> where T : IFacetsObject<T>
{
    IEnumerable<T> ToObject(DataTable obj);
}

In order to get this to work, you also have to change your declaration like this:

public IEnumerable<T> ExecuteQuery<T>(string sql, IFacetsObject<T> obj) 
    where T : IFacetsObject<T>, new()

Upvotes: -1

Related Questions