I need to create code by the IIncrementalGenerator
in at least two projects which are referring to the same library that references the SourceCodeGenerator
project.
My solution, for further clarification:
MySolution
|->DesktopApp
| |->ref:Library
|
|->Library
| |->ref:SourceGenerator
|
|->WebApi
| |->ref:Library
|
|->SourceGenerator
In the library, I define Interfaces for all objects I use across my application, many properties I define in the interface are Ids which I mark by an attribute where I head the IType
of the underlying object.
The attribute:
[System.AttributeUsage(System.AttributeTargets.Property)]
public class NavigationPropertyAttribute : System.Attribute
{
public System.Type NavigationPropertyType { get; }
public NavigationPropertyAttribute(System.Type type) => NavigationPropertyType = type;
}
This is how such an Interface looks like:
public interface IFoo
{
int Id { get; set; }
[NavigationProperty(typeof(IBar))]
int BarId { get; set; }
[NavigationProperty(typeof(IFoo))]
int ParentId { get; set; }
[NavigationProperty(typeof(IFoo))]
int ChildId { get; set; }
}
In both projects, one is a desktop application, the other is a Web API using EF Core, I have partial classes implementing these interfaces.
namespace IGTryout.Main;
public partial class Foo : IFoo
{
public int Id { get; set; }
public int BarId { get; set; }
public int ParentId { get; set; }
public int ChildId { get; set; }
}
what I need now, and where I'm struggling with, is using the IIncrementalGenerator
to create a partial class with properties based on the Id
, Name
and the Type
from the NavigationPropertyAttribute
.
public partial class Foo
{
private Bar bar;
public Bar Bar => bar ??= GetValue<Bar>();
private Foo parent;
public Foo Parent => parent ??= GetValue<Foo>();
private Foo child;
public Foo Child => child ??= GetValue<Foo>();
}
(GetValue<T>()
is an extension which gets the [CallerMemberName]
, resolves the matching Id
via reflection and returns the object from cache by resolving it by the type and id)
In the Web API project, I'm also implementing the interfaces, yet, creating the NavigationProperties
the common way
Foo { get; set; }
to fullfill EF Core's needs, also I'm creating the InversePropertyCollection
in the related object for having the foreign keys set as needed by EF Core.
All of this is done, so I can use the interfaces as TransportObject
with as little overhead as possible when sending them from the Web API to my desktop application and reverse.
Now to the problem I have:
to trigger all changes done to the interfaces, I reference my SourceCodeGenerator
project from the library project, but by doing so, I can not find any opportunity to create the code inside the Web API assembly or the desktop app assembly.
I did solve this problem before by using the common SourceGenerator
and referencing the SourceCodeGenerator
project by both, the desktop app and the Web API, and accessing the interfaces inside my library project by calling the ReferencedAssemblySymbols
of my compilations SourceModule
and iterating over to the relevant project, since it is referenced by both, but this rather hacky solution is not possible, since would loose all the possibilities, which led me to switch to the IIncrementalGenerator
in the first place.
Below is the answer to why this will not work with a Class Library project. however while tying up this answer I learned of the existence of Shared Projects which based on a cursory look at how they work, might enable the behavior of incremental generators on shared source code, see thoughts below.
There are at 2 distinct reasons why what is asked in the question won't work with a Class Library project. One is to use source generator in a way they are not designed to work, and two, even if they worked as you wanted, the generated code would not behave as expected.
By design, Source Generators hook to a compilation. The purpose of projects is to provide a distinct compilation unit. The use of solutions to group projects and order projects is a convivence, but does not cause the individual projects to merge into one compilation. Once a class library project is visible to the downstream projects, it is not longer a bunch of source code, it is just a dll.
The example is making a partial class with the partials in three different projects, so even if you got the source generator to make the files in the correct project you would not end up with what you wanted, which is a WebApp version of Foo
and and Desktop version of Foo
. Instead you would end up with 3 versions of Foo
the Library version with the Id Properties, the WebApp version with the auto properties for the navigations, and the Desktop version with the GetValue
call. with both the desktop and webapp version missing the Id Properties. see this question/answer
Can Shared Projects help here? Maybe. The intent of shared projects is to shared source code, not to be a compilation unit, so a change to the source should trigger the incremental generator in any project referencing that shared source, since that source is now part of that project's compilation.
You will want to be aware of the differences between a class library, and a shared project, and how that can impact your program, as now that code is being compiled multiple times in different contexts, so you can actually end up with completely different functionality from the same source. (i.e. features like global usings could cause namespace resolution to cause a Type to match by name to one in a completely different assembly, e.g. you have a property Document {get;set;}
which in one project resolves to say AutoCAD.Document
, but in another resolves to Microsoft.CodeAnalysis.Document
.)