12

I have the following method:

public static TEventInvocatorParameters Until
    <TEventInvocatorParameters, TEventArgs>(this TEventInvocatorParameters p,
                                            Func<TEventArgs, bool> breakCond)
    where TEventInvocatorParameters : EventInvocatorParameters<TEventArgs>
    where TEventArgs : EventArgs
{
    p.BreakCondition = breakCond;
    return p;
}

And this class

public class EventInvocatorParameters<T>
    where T : EventArgs
{
    public Func<T, bool> BreakCondition { get; set; }
    // Other properties used below omitted for brevity.
}

Now, I have the following problems:

  1. This extension method shows on all types, even string.
  2. I can't write new EventInvocatorParameters<EventArgs>(EventABC).Until(e => false); It is telling me "The type arguments for method ... cannot be inferred from the usage."

Can't I use generic type parameters like this? How would you resolve this problem?
Important point: I need both of those generic parameters, because I need to return the same type this extension method was called on.


Broader picture (not necessary for answering the question!):
I am trying to create a fluent interface to invoking events. The base is this static class:

public static class Fire
{
   public static void Event<TEventArgs>(
       ConfiguredEventInvocatorParameters<TEventArgs> parameters)
    where TEventArgs : EventArgs
    {
        if (parameters.EventHandler == null)
        {
            return;
        }

        var sender = parameters.Sender;
        var eventArgs = parameters.EventArgs;
        var breakCondition = parameters.BreakCondition;

        foreach (EventHandler<TEventArgs> @delegate in 
                 parameters.EventHandler.GetInvocationList())
        {
            try
            {
                @delegate(sender, eventArgs);
                if (breakCondition(eventArgs))
                {
                    break;
                }
            }
            catch (Exception e)
            {
                var exceptionHandler = parameters.ExceptionHandler;
                if (!exceptionHandler(e))
                {
                    throw;
                }
            }
        }
    }
}

To make sure this method can only be called with fully configured parameters, it only accepts a ConfiguredEventInvocatorParameters<T> which derives from EventInvocatorParameters<T>:

public class ConfiguredEventInvocatorParameters<T>
    : EventInvocatorParameters<T>
    where T : EventArgs
{
    public ConfiguredEventInvocatorParameters(
        EventInvocatorParameters<T> parameters, object sender, T eventArgs)
        : base(parameters)
    {
        EventArgs = eventArgs;
        Sender = sender;
    }

    public T EventArgs { get; private set; }
    public object Sender { get; private set; }

}

The following would be valid calls:

Fire.Event(EventName.With(sender, eventArgs));
Fire.Event(EventName.With(sender, eventArgs).Until(e => e.Cancel));
Fire.Event(EventName.Until(e => e.Cancel).With(sender, eventArgs));

The following would be invalid:

// no sender or eventArgs have been specified, i.e. missing call to With(...)
Fire.Event(EventName.Until(e => e.Cancel));

To make this work, there exist extension methods named With, that accept either a EventHandler<TEventArgs or a TEventInvocatorParameters and return a ConfiguredEventInvocatorParameters<TEventArgs>. All calls following the With now also need to return the type ConfiguredEventInvocatorParameters<TEventArgs>, otherwise the second example of a valid call (with the Until at the end) wouldn't work.
If you have any thoughts on the API in general, please let me know. However, I want to avoid the following three things:

  • Fail only at runtime if the parameters have not been configured fully
  • Creating an inverse syntax like EventName.With(...).Until(...).Fire()
  • Use the infamous Do method to start off things: Fire(EventName).With(...).Until(...).Do();
Daniel Hilgarth
  • 171,043
  • 40
  • 335
  • 443

4 Answers4

18

UPDATE from November 2020: The original answer below was written in 2011; the rules for generic method type inference, overload resolution, and how "final validation" of methods is done have had small but significant changes in recent versions of C#; this answer, and the link to an archived article on my original MSDN blog about it might no longer be accurate. Also, Microsoft deleted the comments on the original article for legal reasons; there was a huge amount of context and discussion in those comments. I hope to at some point have the time to revisit this article to clarify (1) the rules today, (2) how they have changed, and (3) how the ideas discussed in those deleted comments influenced those decisions, but that's a lot of work and I may not get to it for some time. Remember, I have not been on the C# language design team since November 2012.


Generic method type inference deliberately does not make any deductions from the constraints. Rather, deductions are made from the arguments and the formal parameters, and then the deduced type arguments are checked against the constraints.

For a detailed discussion of some of the design issues around constraints and method signatures, including several dozen people telling me that I'm wrong to think that the existing design is sensible, see my article on the subject:

https://learn.microsoft.com/en-gb/archive/blogs/ericlippert/constraints-are-not-part-of-the-signature

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • 3
    I liked that part of your answer: "including several dozen people telling me that I'm wrong to think that the existing design is sensible"! :-) Care to provide an alternative that achieves what I want? – Daniel Hilgarth Aug 24 '11 at 06:54
  • And BTW: I agree with them. Without wanting to re-start that discussion: What you say is the best match might be technically so, but intuitively it isn't. All the answers telling you that you are wrong should be enough proof of that. Your main point is invalid in my opinion. There is no guessing going on. I completely agree with David Nelson's answer from 10 Dec 2009 4:13 PM. Your answer to him is not any more convincing. The main problem is that the compiler infers a type that is not valid for that generic method. – Daniel Hilgarth Aug 24 '11 at 07:14
  • @Daniel: The problem is that different scenarios can pump your intuition in different directions. I maintain my position: C# is a language which tells you when something looks wrong. It does not ignore a problem and fall back to a worse choice when the deduced best choice violates a constraint. – Eric Lippert Aug 24 '11 at 07:15
  • 2
    I know that you maintain your position. All those arguments couldn't make you change it, so why should mine? However, you *might* improve understanding if you provide a sample that actually shows what you perceive as the problem. Imagine C# would have been implemented the other way around, what scenarios would then fail to be intuitive? What errors and problems would it cause? Because I - and obviously many others - don't see how this would "ignore a problem". – Daniel Hilgarth Aug 24 '11 at 07:21
  • I always had the intuition that constraints would not be part of the signature. I am not able to show this intuition as causing problems if it is false. My intuition is based on the fact that I expect the word "constraint" to imply that it will reduce the number of legal method calls I can make, rather than change what those method calls do. I envision using a constraint on a method that is undefined for a particular situation but where calling it as an outsider looking at the method signature would appear to otherwise make sense. Take w/grain of salt; I don't use constraints much. – Brian Aug 25 '11 at 15:02
  • I would love it if they could at least allow polymorphism based upon type constraints in this case: void foo(T bar) where T : struct void foo(T bar) where T : class – Thomas S. Trias Mar 29 '12 at 18:10
  • IMO, it's a design flaw. If you have two extension method overloads for a generic type, and you constrain the second overload to type Foo, the compiler isn't smart enough to select the first overload when calling from a source object of type Spam, but it's smart enough to tell you that the source object doesn't match the type constraint. The whole reason for extension methods is convenience, and there's nothing convenient when you can't use them because the compiler isn't smart enough to pick the right overload, yet it's smart enough to tell you that it picked the wrong one. – Stephen Bunch Dec 18 '12 at 18:59
  • The article Eric linked is no longer at that URL. It is available here now: https://learn.microsoft.com/en-gb/archive/blogs/ericlippert/constraints-are-not-part-of-the-signature – Matthew Watson Nov 05 '20 at 15:55
  • @MatthewWatson: Thanks! I will eventually move that over to ericlippert.com, but it is a slow process. – Eric Lippert Nov 05 '20 at 21:03
3

For anyone interested, for now, I solved the original problem (fluent event invocation API) with a generic class hierarchy. This is basically Hightechrider's answer on steroids.

public abstract class EventInvocatorParametersBase
    <TEventInvocatorParameters, TEventArgs>
    where TEventArgs : EventArgs
    where TEventInvocatorParameters :
        EventInvocatorParametersBase<TEventInvocatorParameters, TEventArgs>

{
    protected EventInvocatorParametersBase(
        EventHandler<TEventArgs> eventHandler,
        Func<Exception, bool> exceptionHandler,
        Func<TEventArgs, bool> breakCondition)
    {
        EventHandler = eventHandler;
        ExceptionHandler = exceptionHandler;
        BreakCondition = breakCondition;
    }

    protected EventInvocatorParametersBase(
        EventHandler<TEventArgs> eventHandler)
        : this(eventHandler, e => false, e => false)
    {
    }

    public Func<TEventArgs, bool> BreakCondition { get; set; }
    public EventHandler<TEventArgs> EventHandler { get; set; }
    public Func<Exception, bool> ExceptionHandler { get; set; }

    public TEventInvocatorParameters Until(
        Func<TEventArgs, bool> breakCondition)
    {
        BreakCondition = breakCondition;
        return (TEventInvocatorParameters)this;
    }

    public TEventInvocatorParameters WithExceptionHandler(
        Func<Exception, bool> exceptionHandler)
    {
        ExceptionHandler = exceptionHandler;
        return (TEventInvocatorParameters)this;
    }

    public ConfiguredEventInvocatorParameters<TEventArgs> With(
        object sender, 
        TEventArgs eventArgs)
    {
        return new ConfiguredEventInvocatorParameters<TEventArgs>(
            EventHandler, ExceptionHandler, BreakCondition,
            sender, eventArgs);
    }
}

public class EventInvocatorParameters<T> :
    EventInvocatorParametersBase<EventInvocatorParameters<T>, T>
    where T : EventArgs
{
    public EventInvocatorParameters(EventHandler<T> eventHandler)
        : base(eventHandler)
    {
    }
}

public class ConfiguredEventInvocatorParameters<T> :
    EventInvocatorParametersBase<ConfiguredEventInvocatorParameters<T>, T>
    where T : EventArgs
{
    public ConfiguredEventInvocatorParameters(
        EventHandler<T> eventHandler,
        Func<Exception, bool> exceptionHandler,
        Func<T, bool> breakCondition, object sender,
        T eventArgs)
        : base(eventHandler, exceptionHandler, breakCondition)
    {
        EventArgs = eventArgs;
        Sender = sender;
    }

    public ConfiguredEventInvocatorParameters(EventHandler<T> eventHandler,
                                              object sender,
                                              T eventArgs)
        : this(eventHandler, e => false, e => false, sender, eventArgs)
    {
    }

    public T EventArgs { get; private set; }
    public object Sender { get; private set; }
}

public static class EventExtensions
{
    public static EventInvocatorParameters<TEventArgs> Until<TEventArgs>(
        this EventHandler<TEventArgs> eventHandler,
        Func<TEventArgs, bool> breakCondition)
        where TEventArgs : EventArgs
    {
        return new EventInvocatorParameters<TEventArgs>(eventHandler).
            Until(breakCondition);
    }

    public static EventInvocatorParameters<TEventArgs> 
        WithExceptionHandler<TEventArgs>(
            this EventHandler<TEventArgs> eventHandler,
            Func<Exception, bool> exceptionHandler)
        where TEventArgs : EventArgs
    {
        return
            new EventInvocatorParameters<TEventArgs>(eventHandler).
                WithExceptionHandler(exceptionHandler);
    }

    public static ConfiguredEventInvocatorParameters<TEventArgs>
        With<TEventArgs>(
            this EventHandler<TEventArgs> eventHandler, object sender,
            TEventArgs eventArgs)
        where TEventArgs : EventArgs
    {
        return new ConfiguredEventInvocatorParameters<TEventArgs>(
            eventHandler, sender, eventArgs);
    }
}

This allows you to write code like this:

Fire.Event(EventName.WithExceptionHandler(e => false)
                    .Until(e => false).With(this, EventArgs.Empty));
Fire.Event(EventName.With(this, EventArgs.Empty));
Fire.Event(EventName.WithExceptionHandler(e => false)
                    .With(this, EventArgs.Empty).Until(e => false));
Fire.Event(EventName.With(this, EventArgs.Empty)
                    .WithExceptionHandler(e => false).Until(e => false));

But it doesn't allow you to write this, because not all necessary info (eventArgs and sender) has been provided:

Fire.Event(EventName.Until(e => false));
Fire.Event(EventName);
Daniel Hilgarth
  • 171,043
  • 40
  • 335
  • 443
1

Is there some reason you need to use an extension method? If you put Until on the EventInvocatorParameters<T> class you can avoid both of the problems mentioned:

public class EventInvocatorParameters<T>
    where T : EventArgs
{
    public Func<T, bool> BreakCondition { get; set; }
    // Other properties used below omitted for brevity.

    public EventInvocatorParameters<T> Until (Func<T, bool> breakCond)
    {
        this.BreakCondition = breakCond;
        return this;
    }
}
Ian Mercer
  • 38,490
  • 8
  • 97
  • 133
  • Yes, there is a reason. It is detailed in the long part of the question titled "Broader picture (not necessary for answering the question!)". Maybe it is necessary after all. Short version: `Until` might be called on a derived class of `EventInvocatorParameters` and if that is the case, the return type of `Until` needs to be that derived type. – Daniel Hilgarth Aug 24 '11 at 06:35
0

Bit of a cop-out I know, but have you considered using Rx instead, rather than re-inventing what you appear to be trying to do?

piers7
  • 4,174
  • 34
  • 47