2

I'm writing a fluent api for constructing business data concepts to setup integration and regression tests.

I have the following:

public AndWithContext<T> AndWith(Action<T> withAction)
    {
        withAction(_instance);
        return new AndWithContext<T>(_instance);
    }

public AndWithContext<TOut> AndWith<TOut>(Func<T, TOut> withContextChangeFunc)
    {
        var output = withFunc(_instance);
        return new AndWithContext<TOut>(output);
    }

and an example of how I imagine one would want to use the Action<T> portion of the api:

workOrder = c.Given<WorkOrder>()
                         .With(wo => wo.ReferenceNbr = 1234) 
                         .AndWith(wo => wo.SpecOrder = specOrder)
                         .AndWith(wo => wo.WorkItems = workItems)
                         .Create();

I'm expecting the call to .AndWith(wo => wo.SpecOrder = specOrder) to dispatch to the AndWith(Action<T>) but it's landing on AndWith<TOut>(Func<T, TOut>)

Yes, I know I can make it work as desired by telling users to write with the following convention when assigning property values inside the context:

workOrder = c.Given<WorkOrder>()
             .With(wo => wo.ReferenceNbr = 1234)
             .AndWith(wo => {
                              wo.SpecOrder = specOrder;
                              wo.WorkItems = workItems;
                              return wo;
                             })
             .Create();

But that's not the point.

Also interesting to me was that (wo => {wo.SpecOrder = specOrder;}) routes to the correct overload, but removing the braces and semicolon suddenly makes this behave like Func<T, TOut>.

I'm sure this is happening for my WithContext signatures too, but calls to my with context are all context shifting calls and are purely for readability and the beginning of the statement, so I kinda don't care about those.

Josh Gust
  • 4,102
  • 25
  • 41
  • What happens if you use `T` instead of `TOut`? – Andy Jun 27 '18 at 22:47
  • 2
    `wo.SpecOrder = specOrder` returns the result of that assignment. – mjwills Jun 27 '18 at 22:48
  • @Andy, I also have a "fluent" `Func` signature, but the purpose of having the `Func` is to guide the user to the new context that requires additional setup. If the author provides `AndWith` a func call that returns a different context component, that component needs additional setup before the author is returned to the original context and component types. `parent.With(p => p.Name = "josh").AndWith(p => p.BrotherName = "dave")` is invalid. I want them to say `parent.With(p => p.Name = "j").AndWith(p => p.GetChildren())` where `GetChildren` is an extension method for `Parent`. – Josh Gust Jun 27 '18 at 22:54
  • a) Why doesn't the `AndWith` not also define `T`? b) Why is the implementation not symmetrical? I would expect both to return an instance of `AndWithContext` with an `_instance` with the `TOut` implementation also providing the out value. That you combine the two in different contexts seems counterintuitive to me. – Kirk Woll Jun 27 '18 at 23:04
  • @KirkWoll The `T` is defined on the `WithContext`. The `Func` is effectively a context switch, but it's a context switch that uses the same context definition. I need the component to change because `Parent` sans `Child(ren)` is `Person`, so I want to change the context to a component that will make the author setup `Children` before allowing them to continue setting up the `Parent`. – Josh Gust Jun 27 '18 at 23:13
  • 1
    I agree with @mjwills. The link was very helpful for understanding why it happens, and the need for it to route to the `Action` is low. Downstream authors can code to the convention in tests. – Josh Gust Jun 27 '18 at 23:17
  • Well, since this question was closed, here are my thoughts, @JoshGust: https://imgur.com/a/c68Wr7M – Kirk Woll Jun 27 '18 at 23:21
  • 1
    That is a great suggestion, thanks for sharing it @KirkWoll. – mjwills Jun 27 '18 at 23:24
  • @KirkWoll `.Create` is really only an exit point for the fluent path that returns the component `_instance` in the current context. – Josh Gust Jun 27 '18 at 23:31
  • @JoshGust not totally following you. I believe you are saying it returns either the original `_instance` or the `TOut` value. Is that correct? If so, wouldn't my proposed solution work in most scenarios? (value types would be problematic) – Kirk Woll Jun 27 '18 at 23:32
  • This is fluent style creation, so the problem domain is vague and thus so are the context implementations. – Josh Gust Jun 27 '18 at 23:33
  • To try and clarify the domain: the primary functions that are to be given to the context are predefined extensions on the `_instance`. Given that, i have some control over what the return value is representing (ie a new component that needs addl. setup). Access through the `Given, With, AndWith` and `Create` is not absolutely necessary, but it provides a measure of readability that would otherwise be lacking. – Josh Gust Jun 27 '18 at 23:39

0 Answers0