views:

130

answers:

7

UPDATE: I should have mentioned in the original post that I want to learn more about generics here. I am aware that this can be done by modifying the base class or creating an interface that both document classes implement. But for the sake of this exercise I'm only really interested in solutions that do not require any modification to the document classes or their base class. I thought that the fact that the question involves extension methods would have implied this.

I have written two nearly identical generic extension methods and am trying to figure out how I might refactor them into a single method. They differ only in that one operates on List and the other on List, and the properties I'm interested in are AssetID for AssetDocument and PersonID for PersonDocument. Although AssetDocument and PersonDocument have the same base class the properties are defined in each class so I don't think that helps. I have tried

public static string ToCSVList<T>(this T list) where T : List<PersonDocument>, List<AssetDocument>

thinking I might then be able to test the type and act accordingly but this results in the syntax error

Type parameter 'T' inherits conflicting constraints

These are the methods that I would like to refactor into a single method but perhaps I am simply going overboard and they would best be left as they are. I'd like to hear what you think.

public static string ToCSVList<T>(this T list) where T : List<AssetDocument>
{
  var sb = new StringBuilder(list.Count * 36 + list.Count);
  string delimiter = String.Empty;

  foreach (var document in list)
  {
    sb.Append(delimiter + document.AssetID.ToString());
    delimiter = ",";
  }

  return sb.ToString();
}

public static string ToCSVList<T>(this T list) where T : List<PersonDocument>
{
  var sb = new StringBuilder(list.Count * 36 + list.Count);
  string delimiter = String.Empty;

  foreach (var document in list)
  {
    sb.Append(delimiter + document.PersonID.ToString());
    delimiter = ",";
  }

  return sb.ToString();
}
+3  A: 

I think the way would be to let PersonDocument and AssetDocument inherit from a Document class, which would have an Id property, that stores your current PersonId or AssetId respectivly.

Jens
This is good too, because he already has a base class. Even if the property is declared in the base class, both subclasses can create their own implementations of it.
Benny Jobigan
Good answer but please see my update above.
Steve Crane
+3  A: 

Make an abstraction, such as IDocument or an abstract class BaseDocument which exposes the id (which is the only field you are really using) and make both PersonDocument and AssetDocument implement that. Now make your generic method accept IDocument or BaseDocument instead.

klausbyskov
I was going to suggest this too.
Benny Jobigan
Good answer but please see my update above.
Steve Crane
+1  A: 

I only know java, so I can't give correct syntax, but the general approach should work:

define an interface Document, which gets implemented by PersonDocument and AssetDocument, with the method

String getIdString();

Use a List as a parameter to you method. Note this is java syntax for a List of Something that inherits/extends from Document.

Jens Schauder
Good answer but please see my update above.
Steve Crane
+2  A: 

How do you like this variant (a little bit simplified, but you should get the idea):

using System;
using System.Collections.Generic;
using System.Text;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main()
        {
            var la = new List<AssetDocument> { new AssetDocument() {AssetID = 1} };

            var result = la.ToCSVList(l => l.AssetID.ToString());
        }
    }

    public class AssetDocument
    {
        public int AssetID { get; set; }
    }

    public static class GlobalExtensions
    {
        public static string ToCSVList<T>(this List<T> list, Func<T, string> propResolver)
        {
            var sb = new StringBuilder(list.Count * 36 + list.Count);
            var delimiter = "";

            foreach (var document in list)
            {
                sb.Append(delimiter);
                sb.Append(propResolver(document));
                delimiter = ",";
            }

            return sb.ToString();
        }
    }
}

This would work with any list (in case you don't care about the preallocated memory in StringBuilder even with any IEnumerable).

Update: Even if you want to keep your original extension methods, you can reduce them to one line of code with this.

TToni
Workable but moving more complexity to the caller to save a few duplicate lines of code doesn't really make sense.
Steve Crane
+7  A: 

Your implementation is basically reimplementing string.Join method, so you might try to make it simpler and more generic with some LINQ:

public static string ToCSVList<T>(this IEnumerable<T> collection)
{ return string.Join(",", collection.Select(x => x.ToString()).ToArray()); }

public static string ToCSVList(this IEnumerable<AssetDocument> assets)
{ return assets.Select(a => a.AssetID).ToCSVList(); }

public static string ToCSVList(this IEnumerable<PersonDocument> persons)
{ return persons.Select(p => p.PersonID).ToCSVList(); }
Bojan Resnik
Doh, I missed the more obvious string.Join :-(
Dan Puzey
You're not alone :-)
TToni
I like this solution. It doesn't change the calling code and reduces duplicate code to the bare minimum. I use LINQ quite a lot but really must remember to make sure there isn't a LINQ method before going off and writing my own to do something.
Steve Crane
+2  A: 

What about making your method also take in a delegate to return the document.AssetID.ToString() for that list as appropriate?

Using Lamda expressions this could be reasonably lightweight, if a little ugly. A console application to demonstarate:

    class Program
    {
    static void Main(string[] args)
    {
        List<string> strings = new List<string> { "hello", "world", "this", "is", "my", "list" };
        List<DateTime> dates = new List<DateTime> { DateTime.Now, DateTime.MinValue, DateTime.MaxValue };

        Console.WriteLine(ToCSVList(strings, (string s) => { return s.Length.ToString(); }));
        Console.WriteLine(ToCSVList(dates, (DateTime d) => { return d.ToString(); }));

        Console.ReadLine();
    }

    public static string ToCSVList<T, U>(T list, Func<U, String> f) where T : IList<U>
    {
        var sb = new StringBuilder(list.Count * 36 + list.Count);
        string delimiter = String.Empty;

        foreach (var document in list)
        {
            sb.Append(delimiter + f(document));
            delimiter = ",";
        }

        return sb.ToString();
    }
}

Whether this is the best approach or not, I leave as an exercise for the reader!

Dan Puzey
Workable but moving more complexity to the caller to save a few duplicate lines of code doesn't really make sense.
Steve Crane
Totally agree - hence my final comment. Though it does add some flexibility, I can't imagine it'd be useful :)
Dan Puzey
+1  A: 

You could use Reflection for a bit of Duck Typing action!

I have assumed that your classes are called #class#Document and you want to concatenate the #class#ID properties. If the list contains classes that conform to this naming they will be concatenated. Otherwise they wont.

This is very much how the Rails framework operates, using Convention over Configuration.

Obviously such behaviour is more suited to dynamic languages such as Ruby. Probably the best solution for a more static language such as C# would be to refactor the base classes, use interfaces etc.. But that wasnt in the spec, and for educational purposes this is one way around things!

public static class Extensions
{
    public static string ToCSVList<T> ( this T list ) where T : IList
    {
        var sb = new StringBuilder ( list.Count * 36 + list.Count );
        string delimiter = String.Empty;

        foreach ( var document in list )
        {
            string propertyName = document.GetType ().Name.Replace("Document", "ID");
            PropertyInfo property = document.GetType ().GetProperty ( propertyName );
            if ( property != null )
            {
                string value = property.GetValue ( document, null ).ToString ();

                sb.Append ( delimiter + value );
                delimiter = ",";
            }
        }

        return sb.ToString ();
    }
}

Usage (note no need for inheritance with Duck Typing - also works with any type!) :

public class GroovyDocument
{
    public string GroovyID
    {
        get;
        set;
    }
}

public class AssetDocument
{
    public int AssetID
    {
        get;
        set;
    }
}

...

        List<AssetDocument> docs = new List<AssetDocument> ();
        docs.Add ( new AssetDocument () { AssetID = 3 } );
        docs.Add ( new AssetDocument () { AssetID = 8 } );
        docs.Add ( new AssetDocument () { AssetID = 10 } );

        MessageBox.Show ( docs.ToCSVList () );

        List<GroovyDocument> rocs = new List<GroovyDocument> ();
        rocs.Add ( new GroovyDocument () { GroovyID = "yay" } );
        rocs.Add ( new GroovyDocument () { GroovyID = "boo" } );
        rocs.Add ( new GroovyDocument () { GroovyID = "hurrah" } );

        MessageBox.Show ( rocs.ToCSVList () );

...

Mongus Pong