To async or not to async?
How would you define a library interface for a service that probably will be implemented with an in memory procedure - let's say returning a mapped value to a key you registered programmatically - and a user of your API might want to implement a decorator that needs a 'long running task' - for example you want to log a msg into your db or load additional mapping from a file?
Would you define the interface to return a Task<string> or just a string?
Used examples that first came into mind, but it could be anything really.
@Fandermill I'd go with Task. If it's performance critical, then you could consider both variations.
@mihamarkic But wouldn't that make all public interfaces defined as returning Task as you never know how users implement your interface? I guess it's really how the author of the API thinks it should be used of course.
@Fandermill Yep and yep. There is no magic solution. If you want flexibility, go with Task. If you want performance, go without Task. If you need both, implement both, though this might result in a lot of code duplication, so it might not be feasible - it depends. But again, are this methods be often called, like hammered? If not, I'd still go with Task (implicilty 'or ValueTask').
@Fandermill
"probably not async" means "potentially async"
So you're somewhat pushed into designed for "you might need it" not YAGNI, and making it Task-returning
@anthony_steele Wouldn't that make (almost) all public interface declarations to be Task-returning?
Because you never know what an API user is up to. Of course you have your own ideas about how it should be implemented, but I'm always thinking 'I could see the option to use a database for that'.
@anthony_steele
But that applies to EVERY interface.
IDEALLY interfaces should not care about implementation, and only be shaped by whatever code needs to use them.
No, I don't know a good way to solve this.
@blaue_Fledermaus Any significant method on a public interface in a library, yeah.
Where to draw the line is judgement call. But no, it's not great.
@Fandermill Have you considered ValueTask<string> ?
@kvandermotten ValueTask came in mind when I wrote the toot, but that's not really the point of my question. I see ValueTask as an optimization for Task when used in the right place. But my question is more about using async at all.
@Fandermill Define returning ValueTask<T> which is specifically intended for the case where often, even most,. returns are non-blocking. But sometimes they can. Avoids the overhead of Task<T> in the non-async (ie. in-memory) cases.
@richardcox13 @Fandermill It really depends on how (Value)Task is going to be used though.
@mihamarkic @richardcox13 Yes, but the question is more like 'Are you allowing an async implementation at the cost of (Value)Task overhead, if you don't know how the API user will implement your interface?
I can see the use of ValueTask, but if the user implements a procedure that calls the DB everytime, the ValueTask wouldnt be a right fit, right?
@Fandermill @richardcox13 I was replying to dilemma between ValueTask and Task, written my opinion on your dilemma in other toot :)
@Fandermill @mihamarkic The overhead (as a return) of ValueType<T> is very small – almost trivial – over a T in the non-async case.
This is why ValueType<T> is used for an increasing number of cases in the BCL (it would be more, if only ValueTask<T> had been defined alongside Task<T> back in FX 4.0).
@richardcox13 @Fandermill True, but if you do async, a state machine is generated in the background by compiler regardless what type of Task you are returning. Also ValueTask is more restricted than Task.
@mihamarkic @Fandermill True. But that's the point: if async there is the normal Task overhead, but only if async. This "it is sometimes async. sometimes not" is why ValueTask<T> exists.
@richardcox13 @Fandermill I don't think that's the case. ValueTask exists to avoid GC since it's a struct. Otherwise it behaves like Task.
@mihamarkic @Fandermill This article covers why better than I can in this short space: https://devblogs.microsoft.com/dotnet/understanding-the-whys-whats-and-whens-of-valuetask/
@richardcox13 @Fandermill Yep, but I think there is misunderstanding. When the article mentions sync scenarios, it really means when not using 'async/await' keywords, just the ValueTask as result. i.e. if you have to implement an interface method ValueTask<int> DoSomethingAsnyc() and your implementation doesn't really do anything async, you would omit async/await and simply return an instance of ValueTask<int>. However, this scenario isn't useful for OP where he would have to to async/await.
@mihamarkic @Fandermill
That's not how I read the OP. I read it as, depending on callback/config/... underlying operation maybe sync or async.
Also, remember if your only that gives a Task/ValueTask is the last call in your function, then you don't need await async: just return the result of that op (and thus save alloc of a state machine in your method).
@richardcox13 @Fandermill I imagine OP having a method like ValueTask ProcessAsync(Func<ValueTask> userAction);
if he wants to allow async user code within his method.
@Fandermill Logging is by default synchronous because proper logging frameworks do operations in the background in parallel. async is not free, so if you have actual IO operations like accessing your DB as part of your use case, those should always be async. If you access the memory, you shouldn't.
@maxitb I agree, but the problem is that you don't know what the API users will implement.
@Fandermill I am confused; can you give me an example? Synchronous & Asynchronous use cases are completely separate, it's up API publisher to pick there correct signature depending on implementation requirements.
@maxitb Well, I think my Q boils down to this:
I want to define an interface in my library with a method like 'IEnumerable<string> GetValues()'. An implementation could be getting the values from memory, or an implementation could fetch values from a database. It's up to the API user.
Should I define the method as is, or go for the Task<IEnumerable<string>> GetValues() version?
What if most users will implement an in-process one, but I could see the use of an out-of-process dependency here?
@Fandermill I would actually make two interfaces. The first one has synchronous operations like IEnumerable<string> Enumerate(), while the other interface has IAsyncEnumerable<string> EnumerateAsync(CancelllationToken cancellation=default). The factory class returns either the one or the other interface. Mixing them together is a really bad idea, especially with enumerations.
@Fandermill So basically you have a factory where you get IService CreateMemoryService() and IAsyncService CreateDatabaseService(). Or you provide them as possible DI injections.
@Fandermill If you have a lot of shared synchronous code, inheritance is actually your friend - just make a base class and both service classes inherited from it and use their protected methods and properties. Or you use one service class and put both interfaces on it. In both cases I would make the classes or at least the ctors internal, so an API user can only create them using the factory.
@Fandermill That's basically how I dealt with the issue in the past; with both interface options, you can also make custom mocks for test or allow users to make their own implementations, which you could support in your factory class.
@Fandermill https://sharplab.io/#v2:C4LghgzgtgPgAgJgIwFgBQ64GYAEBLAO2AFMAnAMzAGNicBJAZTIDc8b0BvdASAHpecAFQDyAEWE4AwgHsoUaQRxRiUAEZkIOABZli6AL7pMuQiQrVajAJ4EqTUqxo4Q9e471ouabthxwkWAA8/gAMAHw4AKIEAK7KpGAkAGpgADYxxBAAFACUANw8PPxCYhIAstKktBA2VKQK0jGaEAAOxFR45Gw4EMAx5OTaugZGaL6mZJROdACCNbZubLQujCxLnDy+s/NU0XFkYKqpxMFI4VGx8YnEKemZc7VZkmC2xKmpiXgKgtIA1sSKKgvGjvT4KHAAXhwABNiJQYqlgPlRnwBCJxDgKlU/AAOeoERrNNodLpUHp9AZDKojDBjXCIHCLJwrJkeLzcFr1EhUEjQxlrGi5HAcHD6HDFABCkFoDIUqSshW8xXREjoUBax2URE0aVSOGAOhwVFk8kUyjUGhwGqaVq57V5PS0YCqfNS0gA5t1yJUcKppAb8ERJhYIDTjD1iGliHyGWUVJUrKznPyHEsADSuWqsjbeCakAhpRlZgXEIUisXFYQEeVW6QQCB4I60YDSI1Va44VhgHBTFukBXeTb0gKnc57K7JNIZbI5YXigBUQglohw84EYrDdIjUZjCBwokSh2lSZcrIz1gWJZz3DzBb1DGLqcFs/L4oEVZrLTrDab+tbdUjEhOzwbtewTRUfFwbZanHA4m1HCJYISSc7ggB5bCeYE3g+YAvgIH5/kBLDQVw8EoVheFERfBclxXNdRRwTdxiDcxplZAAxag+wHdktgfS8n1oSR2xION5H7VlcgKbw+MfdwpBE4hBEyYBJPyN8ogADzAdVjh7H1OhwKxGhwAB3F5gD/HAwGhPlxMsfi7BLQN9UNcgYj6alBxkqD0KcwSFMA4gD2AI8IGINTpMg+g/KTYSgvYvBjkijTIm03TaG9Uh8EGYyYjMiyrJsuzKksWLnMIVzMo8mIvMMWlfHCnc/D3DiuITZNVkEzieQ64oAAUYiOL12v7fTsr9ANoTwKoeRwJpaB9LL9zoa9ZIE+T4uuMSEzUyEIgIYhTMxeMJJLKSh0zDalkC65lN6PaIQOo6hBUyKIK2cqAq2kgQrCiLztnJ6cEO46/tUY9Aaiz6djixTEuSwH9pBl6EYBwSLrQfQgA
@maxitb I see what you did. But splitting it into multiple interfaces makes it hard to decorate them. There can only be one method signature and I guess it's the API author that should make a decision: its sync, or its async with a bit of overhead for sync procedures returning a Task.CompletedTask.
@Fandermill Well, if you force async operations into synchronous ones you might up end up with deadlocks. If you make asynchronous operations out of synchronous ones, you end up with increased GC pressure and a ton of overhead (including exception scoping). It's just two completely different things - like you wouldn't box/unbox a value type needlessly or create a big value type instead of using a reference type. It's basically apples and steamships.
@Fandermill
I've struggled with this question a few times, because exposing Task becomes a leaky abstraction, and interfaces shouldn't care about implementation. But I've never found a satisfactory answer.
@blaue_Fledermaus Thanks for your reply, because the term 'leaky abstraction' gives me a ton of results on this subject.
@ploeh
You have written about this kind of stuff, right?
@blaue_Fledermaus Perhaps not about this particular combination, but plenty of things adjacent to it. Robert C. Martin has written about it before me. Applying the Dependency Inversion Principle, its fairly clear that the API should look synchronous (i.e. return a string), because if someone wants a Decorator, they should decorate their own interface, and not one defined by an implementation detail.
@blaue_Fledermaus BTW, I just thought of this recent article, which seems relevant here: https://blog.ploeh.dk/2024/05/06/conservative-codomain-conjecture
@Fandermill Thank you for an interesting question. It inspired me to write an article: https://blog.ploeh.dk/2024/07/08/should-interfaces-be-asynchronous
@ploeh Thank you for your article, such an interesting read!
I went with the notion that it's easier to make it async afterwards than the other way around. And by extend also being conservative about what you send.
Thanks!
@Fandermill Seems like people working on WebAssembly Components are thinking about pretty similar things right now re: the role of async in defining interfaces. Maybe you'll find the work there relevant. https://www.youtube.com/watch?v=y3x4-nQeXxc