52

In my project I have the following three interfaces, which are implemented by classes that manage merging of a variety of business objects that have different structures.

public interface IMerger<TSource, TDestination>
{
    TDestination Merge(TSource source, TDestination destination);
}

public interface ITwoWayMerger<TSource1, TSource2, TDestination>
{
    TDestination Merge(TSource1 source1, TSource2 source2, TDestination destination);
}

public interface IThreeWayMerger<TSource1, TSource2, TSource3, TDestination>
{
    TDestination Merge(TSource1 source1, TSource2 source2, TSource3 source3, TDestination destination);
}

This works well, but I would rather have one IMerger interface which specifies a variable number of TSource parameters, something like this (example below uses params; I know this is not valid C#):

public interface IMerger<params TSources, TDestination>
{
    TDestination Merge(params TSource sources, TDestination destination);
}

It there any way to achieve this, or something functionally equivalent?

Richard Ev
  • 52,939
  • 59
  • 191
  • 278
  • You can't make one interface with a variable number of arguments, but you can give the same name to all your variations (just like the `Func` delegate) – Ben Voigt Feb 20 '12 at 15:30
  • 1
    While that is just an invalid pseudo code, it makes more sense to write that like `public interface IMerger { TDestination Merge(TDestination destination, params TSource sources); }`. `params` should come last. But as Christian Hayter says, having a base class is the only workaround – nawfal Apr 28 '13 at 11:15

5 Answers5

33

You can't. That is a key part of the API. You could, however, do something around the side, such as accepting a Type[] argument. You might also think up some exotic "fluent API / extension method" way of doing it, but to be honest it probably won't be worth it; but something like:

obj.Merge<FirstType>(firstData).Merge<SecondType>(secondData)
     .Merge<ThirdType>(thirdData).Execute<TDestination>(dest);

or with generic type inference:

obj.Merge(firstData).Merge(secondData).Merge(thirdData).Execute(dest);

Each merge step would simple store away the work to do, only accessed by Execute.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • Also, such an approach could indicate a rational "ordering" to the merging, which isn't true in our scenario. – Richard Ev Oct 23 '09 at 11:47
  • 9
    But then, so does "`TSource1 source1, TSource2 source2, TSource3 source3`" from the question... – Marc Gravell Oct 23 '09 at 11:49
  • 2
    My thinking is that source1 .. sourcen indicates "these are the sources", whereas a chain of .Merge calls indicates that the merges are happening in that specific order. – Richard Ev Oct 23 '09 at 12:03
6

It depends on whether you want your objects to be able to merge objects of different types or not.

For a homogeneous merge, all you need is this:

public interface IMerger<TSource, TDestination> {
    TDestination Merge(IEnumerable<TSource> sources, TDestination destination);
}

For a heterogeneous merge, consider requiring all source types to derive from a common base type:

public interface IMerger<TSourceBase, TDestination> {
    TDestination Merge(IEnumerable<TSourceBase> sources, TDestination destination);
}

I don't see any need for a param array, just pass in the collection of objects.

Christian Hayter
  • 30,581
  • 6
  • 72
  • 99
4

The params can only be at the end or argument lists and is syntactic sugar for an array:

public interface IMerger<TSources, TDestination>
{
  TDestination Merge(TDestination destination, params TSource[] sources);
}

If you want to allow any type to be used, just use object[] instead of TSource.

Note: MS had this "problem" also when they did the Expression stuff. They came up with a bunch of delegates Action<> and Func<> with different numbers of generic arguments, but each delegate is another type in fact.

Lucero
  • 59,176
  • 9
  • 122
  • 152
  • My example using params is not actually valid C# since you can't use params in this context, have updated my post to make this clearer – Richard Ev Oct 23 '09 at 11:45
0

The params keyword is only used in a method signature, it's not something that you can decorate a type with. So, the type is still just TSources, and you have to place the parameter decorated with params last in the method signature:

public interface IMerger<TSources, TDestination> {
    TDestination Merge(TDestination destination, params TSources[] sources);
}
Guffa
  • 687,336
  • 108
  • 737
  • 1,005
  • 1
    How is this answering the question? How are `TSource1`, `TSource2` etc equivalent to `TSources`? I think you have taken a cue from OP's final example he posted (the non working pseudo code). – nawfal Apr 28 '13 at 11:14
0

Today, i worked in a deal to automatize MEF, this uses a way to make a variable generic input parameters, encapsulated in delegates :S

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.ComponentModel.Composition.Primitives;

namespace MEFHelper
{
    public static class MEFImporter
    {
        #region Catalog Field

        private readonly static AggregateCatalog _catalog;

        public static AggregateCatalog Catalog { get { return _catalog; } }

        #endregion

        static MEFImporter()
        {
            //An aggregate catalog that combines multiple catalogs
            _catalog = new AggregateCatalog();
            //Adds all the parts found in all assemblies in 
            //the same directory as the executing program
            _catalog.Catalogs.Add(
                new DirectoryCatalog(
                    System.IO.Path.GetDirectoryName(new Uri(
                    System.Reflection.Assembly.GetExecutingAssembly()
                    .CodeBase).AbsolutePath)
            ));
        }

        /// <summary>
        ///  Fill the imports of this object
        /// </summary>
        /// <param name="obj">Object to fill the Imports</param>
        /// <param name="contructorParameters">MEF contructor parameters</param>
        /// <remarks>Use for MEF importing</remarks>
        public static void DoImport(this object obj, params MEFParam[] contructorParameters)
        {
            //Create the CompositionContainer with the parts in the catalog
            CompositionContainer container = new CompositionContainer(Catalog, true);

            //Add the contructor parameters
            if (contructorParameters != null && contructorParameters.Length > 0) 
            {
                foreach (MEFParam mefParam in contructorParameters)
                    if (mefParam != null && mefParam.Parameter != null) mefParam.Parameter(container);
            }

            //Fill the imports of this object
            container.ComposeParts(obj);
        }

        #region MEFParam

        /// <summary>
        /// Creates a Mef Param to do the Import
        /// </summary>
        /// <typeparam name="T">Type of the value to store</typeparam>
        /// <param name="value">Value to store</param>
        /// <param name="key">Optional MEF label</param>
        /// <returns>A MEF paramameter</returns>
        /// <remarks>This retuns a MEF encapsulated parameter in a delegate</remarks>
        public static MEFParam Parameter<T>(T value, string key = null)
        {
            Action<CompositionContainer> param;
            if (string.IsNullOrWhiteSpace(key)) 
                param = p => p.ComposeExportedValue(value);
            else param = p => p.ComposeExportedValue(key, value);
            return new MEFParam(param);
        }

        /// <summary>
        /// Mef Param to do the Import
        /// </summary>
        public class MEFParam
        {
            protected internal MEFParam(Action<CompositionContainer> param)
            {
                this.Parameter = param;
            }
            public Action<CompositionContainer> Parameter { get; private set; }
        }

        #endregion

    }
}

i use this tool to import & resolve MEF objects generically with a extensor (interesting), the taunt: you can add optionally the import constructor parameters, the problem of this is in the function ComposeExportedValue thats uses a generic parameter, you can't add this in a variable params in a function, with this technique, yes! if you try to test: for example...

public class Factory : IDisposable
{

    [Import(typeof(IRepository))]
    private Repository _repository = null;

    public Factory()
    {
        MEFImporter.DoImport(this, MEFImporter.Parameter("hello"));
    }

    public IRepository Repository
    {
        get
        {
            return _repository;
        }
    }

    public void Dispose()
    {
        _repository = null;
    }
}

--- In another assembly

[Export(typeof(IRepository))]
public class Repository : IRepository
{
     string Param;

     [ImportingConstructor]
     public Repository(string param)
     {
         //add breakpoint
         this.Param = param;
     }
}
ModMa
  • 509
  • 4
  • 5