dotnet / roslyn

The Roslyn .NET compiler provides C# and Visual Basic languages with rich code analysis APIs.
https://docs.microsoft.com/dotnet/csharp/roslyn-sdk/
MIT License
19.1k stars 4.04k forks source link

Source generators can't respect DDD #62377

Closed ScottKane closed 2 years ago

ScottKane commented 2 years ago

With the current implementation of source generators, there is no way for generated code to respect a domain driven design.

For example, I have the following projects, both referencing the generator project: Domain and Application

Domain contains a model with the Feature attribute (causes CRUD source to be generated for the given feature):

[Feature]
public class Product
{
    public int Id { get; set; }
    public string Code { get; set; }

To follow proper DDD given only 2 projects for brevity, a lot of the output source should not be available to Domain, but should be available to Application.

Because the generator will run for each project, Domain will get all of the source because it uses the Feature attribute but Application will get nothing.

I think there needs to be some way to have a persisted syntax receiver which would allow the correct projects to get the desired source.

If there's another way to do this I've not found it, because as it stands, you can't even just serialize the state into the generator output directory because one run gets AppData\Local\Temp\VBCSCompiler\AnalyzerAssemblyLoader\92fd63172da84323a0ba94e97aa9857a\6 and the other gets Generators\bin\Debug\netstandard2.0

I get that I could just let the types be in Domain and they would filter through by referencing but they should really be generated into Application

CyrusNajmabadi commented 2 years ago

To follow proper DDD given only 2 projects for brevity, a lot of the output source should not be available to Domain, but should be available to Application.

Why is Domain asking for code to be generated in it then? Why not only have Application run the generator? I'm not really understanding why things are configured in this fashion.

--

For context, we just do what you tell us to do. If you have a generator in Domain and it generates code, then teh code will be generated in Domain. Likewise, if you don't have a generator in Application, then you won't get generated code in it. So it sounds like we're doing exactly what you set things up to do.

ScottKane commented 2 years ago

It's done in this way so that domain entities can be marked as features which will generate all the CRUD required for the feature. Even if the model defining the feature was done in Application, I would have the same problem in code that needs to be elsewhere (there's more projects than Domain/Application) I only mention 2 to get the point across.

Having to create a type in each project for each part of a feature needed wouldn't save much time vs just writing all the CRUD operations manually whereas just marking an entity as a feature and having everything spit out in the right place would save a lot.

This is not really a bug, more of an enhancement to source generators. Having some persisted state or even the option of persisting the syntax receiver between invocations would be nice. That way, other projects can get their generated code without having to mark a type with an attribute per project to get them.

CyrusNajmabadi commented 2 years ago

It's done in this way so that domain entities can be marked as features which will generate all the CRUD required for the feature.

if all the CRUD is required for the feature, then what is the issue here? :)

--

Put another way, if you were writing the code out manually please show us how it would look in each project. Thanks!

ScottKane commented 2 years ago

Small repo giving an example of how the CRUD features would be set out manually showing what classes could be available in each project: https://github.com/ScottKane/SourceGenerators

CyrusNajmabadi commented 2 years ago

What should I be looking at in there? Which are the files showing what would be generated manually? Thanks!

ScottKane commented 2 years ago

That is how the class structure would look if they were to be manually created, what I would like to do is just have a Feature attribute to generate all of that for me.

In the sample the example entity is Domain/Entities/Product.

There would then be commands and queries for the CRUD functionality in Application (AddEditProductCommand, DeleteProductCommand, GetAllProductsQuery, GetProductByIdQuery each with their respective handlers to do work in the DB). Application/Features/Product

Then there would be a mapping profile which maps the commands/queries to the domain entity. Application/Mappings/ProductProfile

There would then be a repository for the product (repository pattern used optionally if not using EF core) Infrastructure/Repositories

Then there would be a controller presenting the endpoints for products Server/Controllers

As you can see there are a lot of classes that I am wanting to generate just from the entity in Domain and they are in various projects.

ScottKane commented 2 years ago

The classes are lacking an actual body as I don't want to build a working api sample with EF core, MediatR etc but it gives you an idea of the layout of the classes I'm trying to generate.

CyrusNajmabadi commented 2 years ago

@ScottKane would you be willing to come to discord to discuss this more? I'm not really understanding waht you're asking for here, and it might be easier to just hash it out with a real-time chat. Thanks!

ScottKane commented 2 years ago

Sure no problem

CyrusNajmabadi commented 2 years ago

ok. discord.gg/csharp. You can DM me using 'metasyntactic'. Thanks! :)

ScottKane commented 2 years ago

@CyrusNajmabadi I DM'd you on discord

CyrusNajmabadi commented 2 years ago

Closing out as working as intended. The right approach is to just example ISymbols in downstream compilations. It is very much by-design that when compiling one compilation there is zero knowledge that any symbols being references even came from C# (and thus could even have their trees be examined). We very much want to keep the separation here and only have 'syntax' be a thing that one has access to from within the compilation being compiled.