Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug]: Refit Does Not Generate Implementations if 3+ Interfaces Share a Name #1654

Open
chrstophr-wltrs opened this issue Feb 12, 2024 · 1 comment
Labels

Comments

@chrstophr-wltrs
Copy link

chrstophr-wltrs commented Feb 12, 2024

Describe the bug 馃悶

Hi all 馃憢

I'm working on integration tests for a handful of distinct-yet-related APIs. These are ASP.NET applications, and they group endpoints into "controllers" based on their related functionality. Some of these APIs have controllers with conflicting names, even though they are in different namespaces (ex "Two APIs each have a UserController").

As I'm working on different APIs, sometimes I run into the following scenario, where I have different namespaces with the same Refit interface:

  • Application.csproj
    • Foo/
      • IUser.cs
    • Bar/
      • IUser.cs
    • Baz/
      • IUser.cs

Foo, Bar, and Baz being different ASP.NET applications in this case. The interfaces are different, they just all have to do with managing users.

If I try to use any of these interfaces, for example with the following code

using Scratch.Application.Foo;

namespace Scratch.Tests.Foo;

public class UserTest
{
	[Test]
	public async Task CanSearchUsers()
	{
		var api = RestService.For<IUser>("http://example.com");
		var response = await api.ViewAll();
	}
}

I get an exception:

System.InvalidOperationException : IUser doesn't look like a Refit interface. Make sure it has at least one method with a Refit HTTP method attribute and Refit is installed in the project.
   at Refit.RestService.GetGeneratedType(Type refitInterfaceType) in /_/Refit/RestService.cs:line 169
   at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
   at Refit.RestService.For(Type refitInterfaceType, HttpClient client, IRequestBuilder builder) in /_/Refit/RestService.cs:line 76
   at Refit.RestService.For[T](HttpClient client, IRequestBuilder`1 builder) in /_/Refit/RestService.cs:line 20
   at Refit.RestService.For[T](HttpClient client, RefitSettings settings) in /_/Refit/RestService.cs:line 34
   at Refit.RestService.For[T](String hostUrl, RefitSettings settings) in /_/Refit/RestService.cs:line 54
   at Refit.RestService.For[T](String hostUrl) in /_/Refit/RestService.cs:line 65
   at Scratch.Tests.Foo.UserTest.CanSearchUsers() in C:\Users\i35531\development\xm8.services.e2e.tests\Scratch\Tests\Foo\UserTest.cs:line 10

The interesting thing is, if you drill down to look at the generated types, the class name includes the full namespace:

/* IUser1.g.cs */
namespace Refit.Implementation
{

    partial class Generated
    {

    /// <inheritdoc />
    [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
    [global::System.Diagnostics.DebuggerNonUserCode]
    [global::ScratchRefitInternalGenerated.PreserveAttribute]
    [global::System.Reflection.Obfuscation(Exclude=true)]
    [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
    partial class ScratchApplicationFooIUser
        : global::Scratch.Application.Foo.IUser

    {

...

Because of this, I would think that name collisions wouldn't be an issue, since the implementing class uses the full namespace of the interface as its name.

The easiest way I've found to track whether the types are being generated is a small annotation that's provided by my IDE, in this case JetBrains Rider:

image

Visual Studio has a similar feature, where you can view the implementations of an interface after building a project. If the annotation is there, then I know Refit is working. If it's absent, I know that something is broken.

Step to reproduce

Follow these steps within a single C# project.

  1. Create 3 or more separate folders with distinct names
  2. In each folder, add an interface with the same name
  3. Add at least one Refit method to each interface- the method names do not matter in this case
  4. Attempt to instantiate any of the classes (ex var api = RestService.For<IApi>(baseUrl);)
  5. Run your application
  6. Note the exception thrown when you attempt to instantiate a class

Reproduction repository

https://github.com/reactiveui/refit

Expected behavior

Each interface should be auto-generated without issue, and accessed by specifying a namespace.

IDE

Visual Studio 2022, Rider Windows

Version

.NET 7, .NET 6

Refit Version

7.0.0

@chrstophr-wltrs
Copy link
Author

Workarounds

There are, of course, a number of workarounds.

Separate Projects

The interfaces for each "API" could be made their own project, with a separate .csproj file.

  • Foo.csproj
    • IUser.cs
  • Bar.csproj
    • IUser.cs
  • Baz.csproj
    • IUser.cs

This could be less convenient, depending on the size of each API, and managing all the cross-project references could become pretty complicated pretty quickly.

Partial Interfaces

Instead of each file being its own interface, the endpoints are all combined onto a single interface.

  • Application.csproj
    • Foo/
      • User.cs
    • Bar/
      • User.cs
    • Baz/
      • User.cs
/* /Foo/User.cs */
public partial interface IFoo
{
	[Get("/user")]
	Task<IApiResponse<List<UserDetails>>> ViewAllUsers();
}

Each other "controller" follows this same pattern, where the endpoints are separated into different files based on related functionality, but they're all put onto the interface of IFoo. The endpoints all need to be renamed for specificity, since they'll be in the namespace. Additionally, Intellisense gets pretty crowded, since the interface has access to all of its hundreds of endpoints at the same time.

Unique Names

Of course, one can always just rename the interfaces in question, so that they no longer have the same name. The scheme my team opted for looked something like this:

  • Application.csproj
    • Foo/
      • User_Foo.cs
    • Bar/
      • User_Bar.cs
    • Baz/
      • User_Baz.cs

Each API interface is suffixed with the name of the API to which it belongs. Everything is kept in the same project so there's only one reference to track, and the namespace isn't as cluttered as partial interfaces, but speaking subjectively, this solution looks a little "ugly".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant