Quantcast
Channel: Andrew Lock | .NET Escapades
Viewing all articles
Browse latest Browse all 743

Disambiguating types with the same name with extern alias

$
0
0

In this post I describe a relatively niche scenario, in which you have references to two libraries that both define a given type. This type has the same name in both libraries (the same type name and namespace), so how does the compiler know which one to use? Well, it doesn't, so you get a compile time error:

Error CS0433 : The type 'Class1' exists in both 'Library1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' and 'Library2, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'

And an error in the IDE:

JetBrains Rider showing an error for the ambigous match

In this short post I show how you can resolve the issue using a piece of little know C# syntax: extern alias.

Reproducing the scenario: duplicate type definitions

Just to make sure we're all on the same page, I'll create a quick demo solution that reproduces the issues. The solution contains 3 projects:

  • Library1—a class library.
  • Library2—a class library.
  • MyApp—a console app.

The MyApp project contains a reference to both of the library projects

I'm using the .NET 8 SDK here but this all works way back, even in .NET Framework.

We can scafold the solution using the .NET CLI:

# Create a folder containing the Library1 class library project
mkdir Library1
cd Library1
dotnet new classlib
cd .. 

# Create a folder containing the Library2 class library project
mkdir Library2
cd Library2
dotnet new classlib
cd ..

# Create a folder containing the MyApp console project
mkdir MyApp
cd MyApp
dotnet new console
# Add a reference to both the library projects
dotnet add reference ../Library1
dotnet add reference ../Library2
cd ..

# Create a solution containing all three projects
dotnet new sln
dotnet sln add Library1
dotnet sln add Library2
dotnet sln add MyApp

Next we update the scaffolded Library1/Class1.cs file to the following:

namespace CommonNamespace;

public class Class1
{
    public static void SayHello() => Console.WriteLine("Hello from Library 1");
}

Similarly we update the Library1/Class2.cs file to something very similar. Note that they don't have to look the same, we just need to both have the same name (Class1) and namespace (CommonNamespace).

namespace CommonNamespace;

public class Class1
{
    public static void SayHello() => Console.WriteLine("Hello from Library 2");
}

If you compile your app now, there's no problems. The two Class1 definitions live in dfferent assemblies, but they're not used directly by MyApp, so there's ambiguity. The problem arises if you try to reference Class1 in MyApp:

using CommonNamespace;

Class1.SayHello();

This gives IDE errors (the IDE doesn't give IntelliSense for either Class1 or Class2 because it doesn't know which to call):

JetBrains Rider showing an error for the ambigous match

and the build fails with an error:

Program.cs(3,1): Error CS0433 : The type 'Class1' exists in both 'Library1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' and 'Library2, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'

In our case it would be an easy fix to just change one of the type names or namespaces. But what if you can't? What if those types are in a different package? Or if they intentionally are kept the same?

Namespace aliases: global:: and friends

One feature of C# that you typically don't need to worry about is that the types and namespaces are all defined within the global namespace alias. In general, all types you create and use will be in the global namespaces alias, so you generally don't have to worry about it. Nevertheless, you might see it pop up if you look at generated code from source generators (for example). So you might see something like this:

public class EfCoreValueConverter : global::Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter<MyId, global::System.Guid>
{
    global::System.Guid Value { get; }
    // ..
}

In the code above you can see that the namespaces are included explictly, and that they're all prefixed with global::. This is saying to the compiler "use the System.Guid type defined in the global namespace alias".

Given there's a global namespace alias, you probably won't be surprised to learn that you can create other named namespace aliases, and that they're going to be the solution to our ambiguity problem!

Assigning a namespace alias to a packge.

You can reference a project or package in your .csproj file using <PackageReference> for NuGet package references or <ProjectReference> for project references:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <!-- 👇 Referencing a project -->
    <ProjectReference Include="..\Library1\Library1.csproj" />
    <ProjectReference Include="..\Library2\Library2.csproj" />
  </ItemGroup>
</Project>

By default, all the types defined in the referenced project are loaded into the global namespace alias. But you can change that, by adding the Aliases property!

We can solve the ambiguity problem by loading one (or both) of the referenced projects into a named namespace alias. The example below shows how we can assign the named alias library1 to the Library1 project:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <!-- Assigning an alias to Library1 so that it's loaded in the library1 namespace -->
    <!--                                                     👇                       -->
    <ProjectReference Include="..\Library1\Library1.csproj" Aliases="library1" />
    <ProjectReference Include="..\Library2\Library2.csproj" />
  </ItemGroup>
</Project>

With this simple change, suddenly we have IntelliSense, and our simple program compiles and runs!

JetBrains Rider showing the code now compiles

When we run our app, it outputs:

Hello from Library 2

So we know that the Class1.SayHello() call in our Program.cs is referencing the Class1 type in Library2. That makes sense—the code in library2 is loaded into the global namespace alias, and that's the alias C# uses by default, so that's the code we reference.

You can also create named namespace aliases in code with a using alias directive, but I'm not going to talk about those in this post.

But what if you want to reference Class1 in Library1? How do you access named namespace aliases?

Referencing named namespace aliases with extern alias

You might think that you can reference the named alias directly in your using statements, for example:

using global::CommonNamespace;   // ✅ Referencing the global namespace works
using library1::CommonNamespace; // ❌ Referencing named namespace fails

global::CommonNamespace.Class1.SayHello();   // ✅ This works too
library1::CommonNamespace.Class1.SayHello(); // ❌ This still fails

Unfortunately, library1:: does not compile.

JetBrains Rider showing the code fails to compile again

It fails to build with the error:

Program.cs(2,7): Error CS0432 : Alias 'library1' not found

To make the library1 namespace alias available, you need to add an extern alias declaration to the top of the file:

extern alias library1;

So the whole Program.cs file becomes:

extern alias library1; // Make the library1 alias available

using library1::CommonNamespace; // Reference the namespace alias

Class1.SayHello();

If we run this code we now get:

Hello from Library 1

Showing that we're correctly referencing the Class1 instance from Library1, hooray! Note that you could add both libraries to different named aliases if you wish, so neither is in the global namespace, if that makes sense for your case.

A couple of tips and tricks

Needless to say, the "overlapping types" problem I'm describing here is generally something you should really try to avoid. It's still cumbersome to use extern alias, and it's still difficult to be sure which type you're referencing without fully checking all the namespaces and namespace aliases.

For example, take the following code:

extern alias library1;

using CommonNamespace;

Class1.SayHello();

This does not use Class1 from Library1, even though we've correctly imported the library1 alias. That's because the using CommonNamespace is referring to the global namespace (by default).

One way I like to get around these things is to use type aliases in these codefiles, to make it very obvious which type we're referring to. For example:

extern alias library1;

using Library1Class1 = library1::CommonNamespace.Class1; // define type aliases
using Library2Class1 = global::CommonNamespace.Class1;

Library1Class1.SayHello(); // Hello from Library 1
Library2Class1.SayHello(); // Hello from Library 2

It's still ugly, but at least it's more explicit!

Another thing to watch out for is typos in the extern alias declaration. There's no type checking for the name here, so if you find your named alias still isn't available, make sure it matches the value provided in the Aliases property of your .csproj.

That's pretty much all there is to cover here, so the last thing to say is: don't use extern alias unless you really need to. You'll know it when you need it, because there will be no other way to solve the problem. If you're not at that stage, don't touch it!

Summary

In this post I described a scenario where a project references a type from two different assemblies, but which have the same namespace and type name. C# loads the referenced assemblies into the global namespace alias by default, so it's impossible to tell them apart.

To solve the problem, you can add the Aliases="MyAlias" property to the <PackageReference> or <ProjectReference> in your csproj, and then add extern alias MyAlias; to the top of the file in which you need to reference the type. Finally, use the MyAlias:: qualifier on namespace references, for example MyAlias::MyNamespace.MyType.


Viewing all articles
Browse latest Browse all 743

Trending Articles