Location>code7788 >text

Generating DTOs with Roslyn's Source Generator

Popularity:503 ℃/2024-11-08 17:18:23

preamble

The benefits of a source generator are many, by generating code at compile time, you can improve the performance of your application by reducing runtime reflection and dynamic code generation, which is sometimes required for programsAOTAs well as trimming compiled dll's is also something that needs to be handled with SG.

We should not be able to get around the Mapper object mapping in our development programs, and the libraries that we use more often are probably theAutoMapper,MaspterThese libraries are very powerful, but because of the internal implementation of reflection, there is no way to develop a program that can be used as an interface to a library.AOTTherefore, if the program is not very complex but has very special needs, it is recommended to use SG to implement Mapper.

Function Demo

Here I demo my own developedAutoDtoGenerate DTO Functions.

For example, if we have a User class, we need to generate a UserDto

public class User
{
	public string Id { get; set; } = null!;
	public string FirstName { get; set; } = null!;
	public string LastName { get; set; } = null!;
	public int? Age { get; set; }
	public string? FullName => $"{FirstName} {LastName}";
}

Define UserDto and label the properties:.

[AutoDto<User>(nameof())]//Here we are assuming that we exclude the Id attribute
public partial record UserDto.

That's it, the source generator will generate the corresponding Dto for us.

partial record class UserDto
{
	/// <inheritdoc cref = ""/>
	public string FirstName { get; set; }
	/// <inheritdoc cref = ""/>
	public string LastName { get; set; }
	/// <inheritdoc cref = ""/>
	public int? Age { get; set; }
	/// <inheritdoc cref = ""/>
}

and also generates a simple Mapper extension method for us: the

public static partial class UserToUserDtoExtentions
{
	/// <summary>
	/// mapper to UserDto
	/// </summary>
	/// <returns></returns>
	public static UserDto MapperToUserDto(this User model)
	{
		return new UserDto()
		{
			FirstName = ,
			LastName = ,
			Age = ,
			FullName = ,
		};
	}
}

Implementation Code

static void GENDTO(Compilation compilation, ImmutableArray<SyntaxNode> nodes, SourceProductionContext context)
{
	if ( == 0) return;
	StringBuilder envStringBuilder = new();
	("// <auto-generated />");
	("using System;");
	("using ;");
	("using ;");
	("using ;");
	("#pragma warning disable");

	foreach (var nodeSyntax in ())
	{
		//Cast<ClassDeclarationSyntax>()
		//Cast<RecordDeclarationSyntax>()
		if (nodeSyntax is not TypeDeclarationSyntax node)
		{
			continue;
		}
		//If it isRecordresemble
		var isRecord = nodeSyntax is RecordDeclarationSyntax;
		//If notpartialKeywords.,then it does not generate
		if (!(x => ()))
		{
			continue;
		}

		AttributeSyntax? attributeSyntax = null;
		foreach (var attr in ())
		{
			var attrName = ()?.();
			if (attrName?.IndexOf(AttributeValueMetadataNameDto, ) == 0)
			{
				attributeSyntax = (x => ().IndexOf(AttributeValueMetadataNameDto, ) == 0);
				break;
			}
		}
		if (attributeSyntax == null)
		{
			continue;
		}
		//freestandingEntityresemble名
		var entityName = ;
		string pattern = @"(?<=<)(?<type>\w+)(?=>)";
		var match = ((), pattern);
		if ()
		{
			entityName = ["type"].(['.']).Last();
		}
		else
		{
			continue;
		}

		var sb = new StringBuilder();
		();
		($"//generate {entityName}-{}");
		();
		("namespace $ni");
		("{");
		("$namespace");
		("$classes");
		("}");
		// ("#pragma warning restore");
		string classTemp = $"partial $isRecord $className {{ $body }}";
		classTemp = ("$isRecord", isRecord ? "record class" : "class");

		{
			// Excluded Properties
			List<string> excapes = [];

			if ( != null)
			{
				for (var i = 0; i < !.; i++)
				{
					var expressionSyntax = [i].Expression;
					if (())
					{
						var name = (expressionSyntax as InvocationExpressionSyntax)!.().First().ToString();
						((['.']).Last());
					}
					else if (())
					{
						var name = (expressionSyntax as LiteralExpressionSyntax)!.;
						(name);
					}
				}
			}
			var className = ;
			var rootNamespace = ;
			//Get the namespace of the file range
			var filescopeNamespace = ().OfType<FileScopedNamespaceDeclarationSyntax>().FirstOrDefault();
			if (filescopeNamespace != null)
			{
				rootNamespace = ();
			}
			else
			{
				rootNamespace = ().OfType<NamespaceDeclarationSyntax>().Single().();
			}
			StringBuilder bodyBuilder = new();
			List<string> namespaces = [];
			StringBuilder bodyInnerBuilder = new();
			StringBuilder mapperBodyBuilder = new();
			();
			List<string> haveProps = [];
			// Generating Properties
			void GenProperty(TypeSyntax @type)
			{
				var symbols = (@(), );

				foreach (ITypeSymbol symbol in <ITypeSymbol>())
				{
					var fullNameSpace = ();
					// namespace (computing)
					if (!(fullNameSpace))
					{
						(fullNameSpace);
					}
					().OfType<IPropertySymbol>().ToList().ForEach(prop =>
																				   {
																					   if (!())
																					   {
																						   // If an attribute with the same name exists,then it does not generate
																						   if (())
																						   {
																							   return;
																						   }

																						   ();

																						   //If it is泛型属性,then it does not generate
																						   if ((x => == ))
																						   {
																							   return;
																						   }

																						   // prop:
																						   var raw = $"public {()} {} {{get;set;}}";
																						   // body:
																						   ($"/// <inheritdoc cref=\"{@type}.{}\" />");
																						   ($"{raw}");

																						   // mapper:
																						   // only ifpublicin order to assign a value to an attribute of
																						   if (?.DeclaredAccessibility == )
																						   {
																							   ($"{} = model.{},");
																						   }
																					   }
																				   });
				}
			}

			// Generating Properties:
			var symbols = (entityName, );
			var symbol = <ITypeSymbol>().FirstOrDefault();
			//References to other libraries.
			if (symbol is null)
				continue;
GenProperty(());

			// generating父resemble的属性:
			INamedTypeSymbol? baseType = ;
			while (baseType != null)
			{
				GenProperty(());
				baseType = ;
			}

			var rawClass = ("$className", className);
			rawClass = ("$body", ());
			// append:
			(rawClass);

			string rawNamespace = ;
			(ns => rawNamespace += $"using {ns};\r\n");

			var source = ();
			source = ("$namespace", rawNamespace);
			source = ("$classes", ());
			source = ("$ni", rootNamespace);

			// generatingMapper
			var mapperSource = ("$namespace", ());
			mapperSource = ("$ns", rootNamespace);
			mapperSource = ("$baseclass", entityName);
			mapperSource = ("$dtoclass", className);
			mapperSource = ("$body", ());

			// incorporation
			source = $"{source}\r\n{mapperSource}";
			(source);
		}
	}

	("#pragma warning restore");
	var envSource = ();
	// format:
	envSource = ();
	($"", (envSource, Encoding.UTF8));
}

const string MapperTemplate = $@"
namespace $namespace
{{
    using $ns ;
    public static partial class $baseclassTo$dtoclassExtentions
    {{
        /// <summary>
        /// mapper to $dtoclass
        /// </summary>
        /// <returns></returns>
        public static $dtoclass MapperTo$dtoclass(this $baseclass model)
        {{
            return new $dtoclass()
            {{
                $body
            }};
        }}
    }}
}}
";

ultimate

The above code completes the entire source generation step, and finally you can use the nuget package I released to experience.

<ItemGroup>
   <PackageReference Include="" Version="1.3.6" />
   <PackageReference Include="" Version="1.5.2" PrivateAssets="all" />
</ItemGroup>

Of course if you're interested in the full implementation you can move to my GitHub repository, welcome star!/vipwan/