Introduce IServiceLocator.TryGetService API method - #1719
Conversation
There was a problem hiding this comment.
Pull Request Overview
This PR introduces a new TryGetService<T> API to complement GetService<T>, updates internal service lookup to return null instead of throwing, and adjusts callers and tests to use the new pattern.
- ServiceManager.GetService now returns
IService?and logs errors instead of throwing. - ServiceContext and
IServiceLocatoradded a genericTryGetService<T>; tests updated to cover bothGetService<T>andTryGetService<T>. - Console runner and Cake script refactored to use the new
TryGetService<T>and reorganized package push logic.
Reviewed Changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/NUnitEngine/nunit.engine/Services/ServiceManager.cs | Changed GetService(Type) to return nullable and defer exception handling |
| src/NUnitEngine/nunit.engine/Services/ServiceContext.cs | Added TryGetService<T> and updated GetService<T> to throw on null |
| src/NUnitEngine/nunit.engine.tests/Services/ServiceManagerTests.cs | Adapted tests to nullable return and used null‐propagating operators |
| src/NUnitEngine/nunit.engine.tests/Api/ServiceLocatorTests.cs | Added tests for generic GetService<T> and TryGetService<T> |
| src/NUnitEngine/nunit.engine.api/IServiceLocator.cs | Extended interface with TryGetService<T> and updated documentation |
| src/NUnitConsole/nunit4-console/Program.cs | Switched to TryGetService<T> for runtime lookup and restructured error branches |
| cake/publishing.cake | Moved package name/path creation into each branch and simplified push logic |
Comments suppressed due to low confidence (2)
src/NUnitEngine/nunit.engine.api/IServiceLocator.cs:26
- Add XML documentation for the generic
<typeparam name="T">and<param name="service">onTryGetService<T>to clarify its contract and nullability behavior.
bool TryGetService<T>(out T? service)
src/NUnitEngine/nunit.engine/Services/ServiceManager.cs:25
- Update the XML doc for
GetService(Type)to indicate that it may return null instead of throwing, matching its new signature.
public IService? GetService(Type serviceType)
| { | ||
| var availableRuntimeService = engine.Services.GetService<IAvailableRuntimes>(); | ||
| if (availableRuntimeService is null) | ||
| if (engine.Services.TryGetService<IAvailableRuntimes>(out var availableRuntimes) && availableRuntimes is not null) |
There was a problem hiding this comment.
[nitpick] The null check availableRuntimes is not null is redundant, since TryGetService<T> already guarantees availableRuntimes is non-null when it returns true.
| if (engine.Services.TryGetService<IAvailableRuntimes>(out var availableRuntimes) && availableRuntimes is not null) | |
| if (engine.Services.TryGetService<IAvailableRuntimes>(out var availableRuntimes)) |
There was a problem hiding this comment.
I had thought it should work without the check for not null, but it didn't. I have a general problem with this pattern. Thoughts?
There was a problem hiding this comment.
Adding the not null when attribute would allow this to work. Copilot seems to be assuming that's the behavior? Maybe it's biased but all the built in .net code that has those attributes on TryX methods?
There was a problem hiding this comment.
Except I already have the attribtue on the TryGetNull out argument. I assume that's why Copilot thinks it's guaranteed.
@manfred-brands I learned the pattern of double-checking from some of your code. Do you know when exactly it's needed and when not?
There was a problem hiding this comment.
@CharliePoole You don't have the attribute on the interface, so calls using the interface don't know there is only one implementation. Adding the attribute there allows us to remove the extra null check.
Not sure which double checking you mean.
The only double checking would be inside a lock where potentially the previous code could have set the variable.
There was a problem hiding this comment.
Ah! The interface. Of course! I'm still not really comfortable with the static analysis having come up in a world where pretty much anything could turn out to be null, i.e. assembly language and C. :-)
I can't point to the code but you used the same pattern if (Tryxxx && something is not null) a few times when you added in nullability checks to the codebase. I think I remembered it because it was unusual. Could have been that you didn't want to or couldn't change an interface.
Thanks for the fix.
|
@manfred-brands I requested a copilot review in addition to see how it works. I'll comment on it's comments but wait for you before I actually make changes. |
veleek
left a comment
There was a problem hiding this comment.
Saw the description by copilot describing the behavior of GetService as the opposite of what it actually is. 😂
ServiceManager.GetService now returns IService? and logs errors instead of throwing.
manfred-brands
left a comment
There was a problem hiding this comment.
I added a commit to add the NotNullWhen attribute on the IServiceLocator interface and removed the double check in Program.cs
The only comment besides the ones from Co-pilot is the name of ServiceManager.GetService which doesn't convey it could return null.
manfred-brands
left a comment
There was a problem hiding this comment.
Thanks. Nothing more from me.
Fixes #897