The method could return a MyService, or an IMyService if you want to be more flexible. It can always create a fresh object, manage and return a singleton object, or manage a pool of objects. For the new and pool cases, you can have another static method for 'returning' the object when the controller/page is done with it, so you can manage disposal or pool availability.
The biggest advantage I see to this approach is in debugging: there is a clear stack trace back to the source of the object, and also to its implementation if you're not using an interface. With DI you often can't tell where an object came from, and sometimes it takes some digging to even find out its exact type (unless you only have one implementation of each interface, which is often the case.)
No registration or configuration is needed, because the types are hard coded. If you need mocking support, a single IsTest config setting could be used in a conditional statement to determine which implementation to return.
That's very little additional coding if you use a ternary statement, and it lets you opt-in to the mocking mechanism if/when/where you need it, instead of permiating your entire architecture with it.
What you do instead is just:
public static IMyService myService;
public static IMyService getMyService() { if myService return myService; else if(isInTest()) return mockedMyService; else return new MyProdService(wtv); }
And call that in your controller that needs to use MyService.
This is different in that it's static. You don't add a service to it, it creates it the first time you ask for it (if singleton), or everytime you ask for it (otherwise), or pools it, etc.
I recon some similarities, but most enterprise "service locator" involve a lot more than this, and often are meant to give you this runtime dynamism where you can load and unload services as the app is running, etc. making them way more complex for apps that don't care about this.
var service = Startup.getMyService();
And potentially know if the controller should dispose service.
This doesn't seem much less "in the way" or "massive" than:
services.AddSingleton<IMyService,MyService>();
When I think DI, I think: //Startup.cs has all the config "goo" I don't want to repeat elsewhere.
services.AddDbContext<MyDbContext>(o=>o.UseSqlServer(config.GetConnectionString("foo"));
services.AddTransient<MyService>();
//MyController.cs (or Page, etc) and others get to use it with no ceremony:
public MyController(MyService foo){ this.foo = foo; }
vs no DI, where the config "goo" has to be repeated in every place I intend to use it (or in this case it could also be buried in MyService.cs):
public MyController(){this.foo = new MyService(new MyDbContext(ConfigurationManager.GetConnectionString("foo")));}
public Dispose(){this.foo.Dispose();}
If I knew MyService would never be swapped out and only ever used in one place, I may opt for the no-DI route. I'll often start there then move it to DI when I want testability, reuse of config, plugability, etc..