baseline: OpenMU upstream b5a0961 (fresh source)

This commit is contained in:
Acentech Dev
2026-07-14 19:00:35 +03:00
parent 450402e47d
commit 36fc125d5c
11968 changed files with 748705 additions and 0 deletions

View File

@@ -0,0 +1,263 @@
// <copyright file="BasicModelGenerator.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Persistence.SourceGenerator;
using System.Reflection;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using MUnique.OpenMU.Annotations;
/// <summary>
/// A generator for the plain and simple objects for the persistence project.
/// </summary>
[Generator]
public class BasicModelGenerator : IIncrementalGenerator, IUnboundSourceGenerator
{
/// <summary>
/// Holds the Assembly-Name which is the target of this generator.
/// </summary>
internal const string TargetAssemblyName = "MUnique.OpenMU.Persistence";
/// <inheritdoc />
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var assemblyNameProvider = context.CompilationProvider.Select((compilation, _) => compilation.AssemblyName);
context.RegisterSourceOutput(assemblyNameProvider, (sourceProductionContext, assemblyName) =>
{
if (!(assemblyName?.EndsWith("Persistence") ?? false))
{
return;
}
try
{
foreach (var (name, source) in this.GenerateSources())
{
sourceProductionContext.AddSource(name, SourceText.From(source, Encoding.UTF8));
}
}
catch (Exception e)
{
sourceProductionContext.ReportDiagnostic(
Diagnostic.Create(
new DiagnosticDescriptor(
"BASICGEN001",
"Source generation failed",
$"{e.GetType()}: {e.Message}",
"SourceGeneration",
DiagnosticSeverity.Error,
true),
Location.None));
}
});
}
/// <summary>
/// Generates the source files.
/// </summary>
/// <returns>The created source files.</returns>
public IEnumerable<(string Name, string Source)> GenerateSources()
{
foreach (var type in ModelGeneratorHelper.CustomTypes)
{
var className = type.Name;
var fullName = type.FullName;
var isCloneable = type.GetCustomAttribute<CloneableAttribute>(true) is not null;
var classSource = $@"{string.Format(ModelGeneratorHelper.FileHeaderTemplate, className)}
namespace MUnique.OpenMU.Persistence.BasicModel;
using MUnique.OpenMU.Persistence.Json;
/// <summary>
/// A plain implementation of <see cref=""{className}""/>.
/// </summary>
public partial class {className} : {fullName}, IIdentifiable, IConvertibleTo<{className}>
{{
{this.CreateConstructors(type)}
{this.CreateIdPropertyIfRequired(type)}
{this.CreateNavigationProperties(type)}
{(isCloneable ? ModelGeneratorHelper.OverrideClonable(type, className) : null)}
/// <inheritdoc/>
public override bool Equals(object obj)
{{
var baseObject = obj as IIdentifiable;
if (baseObject != null)
{{
return baseObject.Id == this.Id;
}}
return base.Equals(obj);
}}
/// <inheritdoc/>
public override int GetHashCode()
{{
return this.Id.GetHashCode();
}}
/// <inheritdoc/>
public {className} Convert() => this;
}}
";
yield return (className, classSource);
}
}
/// <summary>
/// Builds the wrapper code for the navigation properties.
/// </summary>
/// <param name="type">The type whose properties should be handled.</param>
/// <returns>The generated code of the properties.</returns>
private string CreateNavigationProperties(Type type)
{
var result = new StringBuilder();
var virtualNavigationProperties = type.GetProperties()
.Where(p => p.GetGetMethod() is { IsVirtual: true, IsFinal: false }
&& !p.PropertyType.IsValueType
&& !p.PropertyType.IsArray).ToList();
var collectionProperties = virtualNavigationProperties
.Where(p => p.PropertyType.IsGenericType
&& (p.PropertyType.GetGenericTypeDefinition() == typeof(ICollection<>) || p.PropertyType.GetGenericTypeDefinition() == typeof(IList<>))
&& !p.PropertyType.GenericTypeArguments[0].IsPrimitive);
foreach (var property in collectionProperties)
{
result.AppendLine(this.BuildCollectionCode(property));
}
var navigationProperties = virtualNavigationProperties.Where(p => !p.PropertyType.IsGenericType);
foreach (var property in navigationProperties)
{
result.AppendLine(this.BuildNavigationCode(property));
}
return result.ToString();
}
/// <summary>
/// Builds the wrapper code for a simple navigation property.
/// </summary>
/// <param name="property">The handled original property.</param>
/// <returns>The created code.</returns>
private string BuildNavigationCode(PropertyInfo property)
{
var propertyTypeName = property.PropertyType.Name.Split('.').Last();
var propertyType = property.PropertyType;
return $@"
/// <summary>
/// Gets the raw object of <see cref=""{property.Name}"" />.
/// </summary>
[System.Text.Json.Serialization.JsonPropertyName(""{property.Name.ToCamelCase()}"")]
public {propertyTypeName} Raw{property.Name}
{{
get => base.{property.Name} as {propertyTypeName};
{(property.GetSetMethod(true) is { } ? $"set => base.{property.Name} = value;" : null)}
}}
/// <inheritdoc/>
[System.Text.Json.Serialization.JsonIgnore]
public override {propertyType.FullName} {property.Name}
{{
get => base.{property.Name};
{(property.GetSetMethod(true) is { } ? $"{(property.GetSetMethod() is null ? "protected " : null)}set => base.{property.Name} = value;" : null)}
}}";
}
/// <summary>
/// Builds the wrapper code for a collection navigation property.
/// </summary>
/// <param name="property">The handled original property.</param>
/// <returns>The created code.</returns>
private string BuildCollectionCode(PropertyInfo property)
{
var propertyType = property.PropertyType;
var persistentClassName = propertyType.GetGenericArguments()[0].Name;
var originalClassName = propertyType.GetGenericArguments()[0].FullName;
var originalPropertyTypeName = propertyType.GetCSharpFullName();
var propertyTypeName = propertyType.GetCSharpName();
var adapterClass = propertyType.GetGenericTypeDefinition() == typeof(IList<>) ? "ListAdapter" : "CollectionAdapter";
return $@"
/// <summary>
/// Gets the raw collection of <see cref=""{property.Name}"" />.
/// </summary>
[System.Text.Json.Serialization.JsonPropertyName(""{property.Name.ToCamelCase()}"")]
public {propertyTypeName} Raw{property.Name} {{ get; }} = new List<{persistentClassName}>();
/// <inheritdoc/>
[System.Text.Json.Serialization.JsonIgnore]
public override {originalPropertyTypeName} {property.Name}
{{
get => base.{property.Name} ??= new {adapterClass}<{originalClassName}, {persistentClassName}>(this.Raw{property.Name});
protected set
{{
this.{property.Name}.Clear();
foreach (var item in value)
{{
this.{property.Name}.Add(item);
}}
}}
}}";
}
/// <summary>
/// Builds the code for an Id-Property, if the type has none yet.
/// </summary>
/// <param name="type">The handled type.</param>
/// <returns>The created code.</returns>
private string CreateIdPropertyIfRequired(Type type)
{
if (type.GetProperty("Id") is null)
{
return @"/// <summary>
/// Gets or sets the identifier of this instance.
/// </summary>
public Guid Id { get; set; }";
}
return string.Empty;
}
/// <summary>
/// Creates the constructors for the new type, if required.
/// </summary>
/// <param name="type">The inherited type.</param>
/// <returns>The constructors.</returns>
private string CreateConstructors(Type type)
{
var stringBuilder = new StringBuilder();
var className = type.Name;
if (type.GetConstructors().Any(c => c.IsPublic && c.GetParameters().Length > 0)
&& type.GetConstructors().Any(c => c.GetParameters().Length == 0))
{
stringBuilder.AppendLine(@$"/// <inheritdoc />
public {className}()
{{
}}");
}
foreach (var constructor in type.GetConstructors()
.Where(c => c.IsPublic && c.GetParameters().Length > 0))
{
var parameters = constructor.GetParameters();
stringBuilder.AppendLine(@$"
/// <inheritdoc />
public {className}({ModelGeneratorHelper.GetParameterDefinitions(parameters)})
: base({ModelGeneratorHelper.GetParameters(parameters)})
{{
}}");
}
return stringBuilder.ToString();
}
}

View File

@@ -0,0 +1,536 @@
// <copyright file="EfCoreModelGenerator.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Persistence.SourceGenerator;
using System.Reflection;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using MUnique.OpenMU.Annotations;
using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.DataModel.Composition;
/// <summary>
/// Source Generator which creates classes of the our entities specifically for the entity framework core.
/// </summary>
[Generator]
public class EfCoreModelGenerator : IIncrementalGenerator, IUnboundSourceGenerator
{
/// <summary>
/// Holds the Assembly-Name which is the target of this generator.
/// </summary>
internal const string TargetAssemblyName = "MUnique.OpenMU.Persistence.EntityFramework";
private const string GameConfigurationFullName = "MUnique.OpenMU.DataModel.Configuration.GameConfiguration";
private static readonly Type[] IgnoredTypes = { typeof(SimpleElement) };
/// <summary>
/// The standalone types which should not contain additional foreign key, because they were used somewhere in collections (except at GameConfiguration).
/// For these types, join entity classes will be created and ManyToManyCollectionAdapter{T,TJoin} are used adapt between these types and the join entities.
/// </summary>
private static readonly (string TypeName, bool StandaloneForEntityOnly)[] StandaloneTypes =
{
("MUnique.OpenMU.DataModel.Configuration.CharacterClass", false),
("MUnique.OpenMU.DataModel.Configuration.DropItemGroup", false),
("MUnique.OpenMU.DataModel.Configuration.Items.IncreasableItemOption", false),
("MUnique.OpenMU.DataModel.Configuration.Items.ItemDefinition", false),
("MUnique.OpenMU.DataModel.Configuration.Items.ItemOption", false),
("MUnique.OpenMU.DataModel.Configuration.Items.ItemOptionType", false),
("MUnique.OpenMU.DataModel.Configuration.Items.ItemOptionDefinition", false),
("MUnique.OpenMU.DataModel.Configuration.Items.ItemSetGroup", false),
("MUnique.OpenMU.DataModel.Configuration.Items.ItemOfItemSet", true),
("MUnique.OpenMU.DataModel.Configuration.MasterSkillDefinition", false),
("MUnique.OpenMU.DataModel.Configuration.Skill", false),
("MUnique.OpenMU.DataModel.Configuration.GameMapDefinition", false),
};
/// <inheritdoc />
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var assemblyNameProvider = context.CompilationProvider.Select((compilation, _) => compilation.AssemblyName);
context.RegisterSourceOutput(assemblyNameProvider, (sourceProductionContext, assemblyName) =>
{
if (assemblyName != TargetAssemblyName)
{
return;
}
try
{
foreach (var (name, source) in this.GenerateSources())
{
sourceProductionContext.AddSource(name, SourceText.From(source, Encoding.UTF8));
}
}
catch (Exception e)
{
sourceProductionContext.ReportDiagnostic(
Diagnostic.Create(
new DiagnosticDescriptor(
"EFCOREGEN001",
"Source generation failed",
$"{e.GetType()}: {e.Message}",
"SourceGeneration",
DiagnosticSeverity.Error,
true),
Location.None));
}
});
}
/// <summary>
/// Generates the source files.
/// </summary>
/// <returns>The created source files.</returns>
public IEnumerable<(string Name, string Source)> GenerateSources()
{
foreach (var type in ModelGeneratorHelper.CustomTypes)
{
var className = type.Name;
var fullName = type.FullName;
var standaloneCollectionProperties = this.GetStandaloneCollectionProperties(type).ToList();
var isCloneable = type.GetCustomAttribute<CloneableAttribute>(true) is not null;
var classSource = $@"{string.Format(ModelGeneratorHelper.FileHeaderTemplate, className)}
namespace MUnique.OpenMU.Persistence.EntityFramework.Model;
using System.ComponentModel.DataAnnotations.Schema;
using MUnique.OpenMU.Persistence;
/// <summary>
/// The Entity Framework Core implementation of <see cref=""{type.FullName}""/>.
/// </summary>
[Table(nameof({type.Name}), Schema = {(ModelGeneratorHelper.IsConfigurationType(type) ? "SchemaNames.Configuration" : "SchemaNames.AccountData")})]
internal partial class {className} : {fullName}, IIdentifiable
{{
{this.CreateConstructors(type, standaloneCollectionProperties.Any())}
{this.CreateIdPropertyIfRequired(type)}
{this.CreateNavigationProperties(type)}
{(isCloneable ? ModelGeneratorHelper.OverrideClonable(type, className) : null)}
/// <inheritdoc/>
public override bool Equals(object obj)
{{
var baseObject = obj as IIdentifiable;
if (baseObject != null)
{{
return baseObject.Id == this.Id;
}}
return base.Equals(obj);
}}
/// <inheritdoc/>
public override int GetHashCode()
{{
return this.Id.GetHashCode();
}}
{this.CreateInitJoinCollections(type, standaloneCollectionProperties)}
}}
";
yield return (className, classSource);
}
yield return ("ExtendedTypeContext", this.GenerateDbContext());
yield return ("MapsterConfigurator", this.GenerateMapsterConfigurator());
foreach (var (name, source) in this.GenerateJoinEntities())
{
yield return (name, source);
}
}
private static bool IsMemberOfAggregate(PropertyInfo propertyInfo)
{
if (propertyInfo?.Name.StartsWith("Raw") ?? false)
{
propertyInfo = propertyInfo.DeclaringType?.GetProperty(propertyInfo.Name.Substring(3), BindingFlags.Instance | BindingFlags.Public);
}
return propertyInfo?.GetCustomAttribute<MemberOfAggregateAttribute>() is { };
}
private static bool IsStandaloneType(string typeName, Type referencingType)
{
return StandaloneTypes.Any(st =>
{
if (st.TypeName != typeName)
{
return false;
}
return !st.StandaloneForEntityOnly || !ModelGeneratorHelper.IsConfigurationType(referencingType);
});
}
private IEnumerable<(string Name, string Source)> GenerateJoinEntities()
{
var standaloneCollectionProperties = ModelGeneratorHelper.CustomTypes.SelectMany(this.GetStandaloneCollectionProperties).ToList();
foreach (PropertyInfo propertyInfo in standaloneCollectionProperties)
{
var elementType = propertyInfo.PropertyType.GenericTypeArguments[0];
var joinTypeName = propertyInfo.ReflectedType!.Name + elementType.Name;
var source = $@"{string.Format(ModelGeneratorHelper.FileHeaderTemplate, joinTypeName)}
namespace MUnique.OpenMU.Persistence.EntityFramework.Model;
using System.ComponentModel.DataAnnotations.Schema;
using MUnique.OpenMU.Persistence;
using MUnique.OpenMU.Persistence.EntityFramework;
[Table(nameof({joinTypeName}), Schema = {(ModelGeneratorHelper.IsConfigurationType(propertyInfo.ReflectedType) ? "SchemaNames.Configuration" : "SchemaNames.AccountData")})]
internal partial class {joinTypeName}
{{
public Guid {propertyInfo.ReflectedType.Name}Id {{ get; set; }}
public {propertyInfo.ReflectedType.Name} {propertyInfo.ReflectedType.Name} {{ get; set; }}
public Guid {elementType.Name}Id {{ get; set; }}
public {elementType.Name} {elementType.Name} {{ get; set; }}
}}
internal partial class {propertyInfo.ReflectedType.Name}
{{
public ICollection<{joinTypeName}> Joined{propertyInfo.Name} {{ get; }} = new EntityFramework.List<{joinTypeName}>();
}}
";
yield return (joinTypeName, source);
}
}
private string GenerateMapsterConfigurator()
{
var configs = new StringBuilder();
foreach (var type in ModelGeneratorHelper.CustomTypes)
{
configs
.AppendLine($" Mapster.TypeAdapterConfig.GlobalSettings.NewConfig<{type.FullName}, {type.FullName}>()")
.AppendLine($" .Include<{type.Name}, BasicModel.{type.Name}>();")
.AppendLine();
}
var source = $@"{string.Format(ModelGeneratorHelper.FileHeaderTemplate, "MapsterConfigurator")}
namespace MUnique.OpenMU.Persistence.EntityFramework.Model;
using MUnique.OpenMU.Persistence;
using Mapster;
/// <summary>
/// Configures Mapster to properly map these classes to the Persistence.BasicModel.
/// </summary>
public static class MapsterConfigurator
{{
private static bool isConfigured;
/// <summary>
/// Ensures that Mapster is configured to properly map these EF-Core persistence classes to the Persistence.BasicModel.
/// </summary>
public static void EnsureConfigured()
{{
if (isConfigured)
{{
return;
}}
Mapster.TypeAdapterConfig.GlobalSettings.Default.PreserveReference(true);
Mapster.TypeAdapterConfig.GlobalSettings.Default.IgnoreMember((member, side) => member.Name.StartsWith(""Raw""));
{configs}
isConfigured = true;
}}
}}
";
return source;
}
private string GenerateDbContext()
{
var ignores = new StringBuilder();
foreach (var type in ModelGeneratorHelper.CustomTypes)
{
ignores.AppendLine($" modelBuilder.Ignore<{type.FullName}>();");
}
var joinDefinitions = new StringBuilder();
var allStandaloneCollectionProperties = ModelGeneratorHelper.CustomTypes
.Where(t => t.FullName != GameConfigurationFullName)
.SelectMany(t => t.GetProperties().Where(p =>
p.PropertyType.IsGenericType &&
p.PropertyType.GetGenericTypeDefinition() == typeof(ICollection<>) &&
!IsMemberOfAggregate(p) &&
IsStandaloneType(p.PropertyType.GenericTypeArguments[0].FullName, t))).ToList();
foreach (PropertyInfo propertyInfo in allStandaloneCollectionProperties)
{
var elementType = propertyInfo.PropertyType.GenericTypeArguments[0];
var joinTypeName = propertyInfo.ReflectedType!.Name + elementType.Name;
joinDefinitions
.AppendLine($" modelBuilder.Entity<{propertyInfo.ReflectedType.Name}>().HasMany(entity => entity.Joined{propertyInfo.Name}).WithOne(join => join.{propertyInfo.ReflectedType.Name});")
.AppendLine($" modelBuilder.Entity<{joinTypeName}>().HasKey(join => new {{ join.{propertyInfo.ReflectedType.Name}Id, join.{elementType.Name}Id }});");
}
var deleteCascades = new StringBuilder();
deleteCascades.AppendLine(" // All members which are marked with the MemberOfAggregateAttribute, should be defined with ON DELETE CASCADE.");
foreach (var type in ModelGeneratorHelper.CustomTypes)
{
foreach (var propertyInfo in type.GetProperties()
.Where(p => p.GetCustomAttribute<MemberOfAggregateAttribute>() is { })
.Where(p => !IgnoredTypes.Contains(p.PropertyType)))
{
var propertyType = propertyInfo.PropertyType;
var isCollection = propertyType.IsGenericType;
if (isCollection)
{
deleteCascades.AppendLine($" modelBuilder.Entity<{type.Name}>().HasMany(entity => entity.Raw{propertyInfo.Name}).WithOne().OnDelete(DeleteBehavior.Cascade);");
}
else
{
deleteCascades.AppendLine($" modelBuilder.Entity<{type.Name}>().HasOne(entity => entity.Raw{propertyInfo.Name}).WithOne().OnDelete(DeleteBehavior.Cascade);");
}
}
}
var source = $@"{string.Format(ModelGeneratorHelper.FileHeaderTemplate, "ExtendedTypeContext")}
namespace MUnique.OpenMU.Persistence.EntityFramework.Model;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
using MUnique.OpenMU.Persistence;
/// <summary>
/// DbContext which sets all extended base types to ignore.
/// </summary>
public class ExtendedTypeContext : Microsoft.EntityFrameworkCore.DbContext
{{
/// <inheritdoc/>
protected override void OnModelCreating(Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder)
{{
{ignores}
{deleteCascades}
}}
/// <summary>
/// Adds the generated join definitions.
/// </summary>
/// <param name=""modelBuilder"">The model builder.</param>
protected void AddJoinDefinitions(Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder)
{{
{joinDefinitions}
}}
}}
";
return source;
}
private string CreateInitJoinCollections(Type type, ICollection<PropertyInfo> standaloneCollectionProperties)
{
if (!standaloneCollectionProperties.Any())
{
return null;
}
var result = new StringBuilder().AppendLine(@"protected void InitJoinCollections()
{");
foreach (PropertyInfo propertyInfo in standaloneCollectionProperties)
{
var elementType = propertyInfo.PropertyType.GenericTypeArguments[0];
var joinTypeName = propertyInfo.ReflectedType!.Name + elementType.Name;
result.AppendLine($@" this.{propertyInfo.Name} = new ManyToManyCollectionAdapter<{elementType.FullName}, {joinTypeName}>(this.Joined{propertyInfo.Name}, joinEntity => joinEntity.{elementType.Name}, entity => new {joinTypeName} {{ {type.Name} = this, {type.Name}Id = this.Id, {elementType.Name} = ({elementType.Name})entity, {elementType.Name}Id = (({elementType.Name})entity).Id}});");
}
result.Append(" }");
return result.ToString();
}
private string CreateNavigationProperties(Type type)
{
var result = new StringBuilder();
var virtualNavigationProperties = type
.GetProperties()
.Where(p => p.GetGetMethod() is { IsVirtual: true, IsFinal: false }
&& !p.PropertyType.IsValueType
&& !p.PropertyType.IsArray)
.Where(p => type.FullName == GameConfigurationFullName ||
!(p.PropertyType.IsGenericType
&& p.PropertyType.GetGenericTypeDefinition() == typeof(ICollection<>)
&& !IsMemberOfAggregate(p)
&& IsStandaloneType(p.PropertyType.GenericTypeArguments[0].FullName, type)))
.ToList();
var collectionProperties = virtualNavigationProperties
.Where(p => p.PropertyType.IsGenericType
&& (p.PropertyType.GetGenericTypeDefinition() == typeof(ICollection<>)
|| p.PropertyType.GetGenericTypeDefinition() == typeof(IList<>)))
.ToList();
var primitiveCollectionProperties = collectionProperties.Where(p => p.PropertyType.GenericTypeArguments[0].IsPrimitive);
var nonPrimitiveCollectionProperties = collectionProperties.Where(p => !p.PropertyType.GenericTypeArguments[0].IsPrimitive);
foreach (var property in nonPrimitiveCollectionProperties)
{
result.AppendLine(this.BuildCollectionCode(property));
}
foreach (var property in primitiveCollectionProperties)
{
result.AppendLine(this.BuildPrimitiveCollectionCode(property));
}
var navigationProperties = virtualNavigationProperties.Where(p => !p.PropertyType.IsGenericType);
foreach (var property in navigationProperties)
{
result.AppendLine(this.BuildNavigationCode(property));
}
return result.ToString();
}
private string BuildNavigationCode(PropertyInfo property)
{
var propertyTypeName = property.PropertyType.Name.Split('.').Last();
var propertyType = property.PropertyType;
return $@"
/// <summary>
/// Gets or sets the identifier of <see cref=""{property.Name}""/>.
/// </summary>
public Guid? {property.Name}Id {{ get; set; }}
/// <summary>
/// Gets the raw object of <see cref=""{property.Name}"" />.
/// </summary>
[ForeignKey(nameof({property.Name}Id))]
public {propertyTypeName} Raw{property.Name}
{{
get => base.{property.Name} as {propertyTypeName};
{(property.GetSetMethod(true) is { } ? $"set => base.{property.Name} = value;" : null)}
}}
/// <inheritdoc/>
[NotMapped]
public override {propertyType.FullName} {property.Name}
{{
get => base.{property.Name};{(property.GetSetMethod(true) is { } ? $@"{(property.GetSetMethod() is null ? "protected " : null)}set
{{
base.{property.Name} = value;
this.{property.Name}Id = this.Raw{property.Name}?.Id;
}}" : null)}
}}";
}
private string BuildCollectionCode(PropertyInfo property)
{
var propertyType = property.PropertyType;
var persistentClassName = propertyType.GetGenericArguments()[0].Name;
var originalClassName = propertyType.GetGenericArguments()[0].FullName;
var originalPropertyTypeName = propertyType.Name.Split('`')[0] + "<" + originalClassName + ">";
var propertyTypeName = propertyType.Name.Split('`')[0] + "<" + persistentClassName + ">";
var adapterClass = propertyType.GetGenericTypeDefinition() == typeof(IList<>) ? "ListAdapter" : "CollectionAdapter";
return $@"
/// <summary>
/// Gets the raw collection of <see cref=""{property.Name}"" />.
/// </summary>
public {propertyTypeName} Raw{property.Name} {{ get; }} = new EntityFramework.List<{persistentClassName}>();
/// <inheritdoc/>
[NotMapped]
public override {originalPropertyTypeName} {property.Name} => base.{property.Name} ??= new {adapterClass}<{originalClassName}, {persistentClassName}>(this.Raw{property.Name});";
}
private string BuildPrimitiveCollectionCode(PropertyInfo property)
{
var propertyType = property.PropertyType;
var itemTypeName = propertyType.GetGenericArguments()[0].FullName;
var originalPropertyTypeName = propertyType.Name.Split('`')[0] + "<" + itemTypeName + ">";
return $@"
/// <summary>
/// Gets the raw string of <see cref=""{property.Name}"" />.
/// </summary>
[Column(nameof({property.Name}))]
[System.Text.Json.Serialization.JsonPropertyName(""{property.Name.ToCamelCase()}"")]
public string Raw{property.Name} {{ get; set; }}
/// <inheritdoc/>
[System.Text.Json.Serialization.JsonIgnore]
[NotMapped]
public override {originalPropertyTypeName} {property.Name}
{{
get => base.{property.Name} ??= new CollectionToStringAdapter<{itemTypeName}>(this.Raw{property.Name}, newString => this.Raw{property.Name} = newString);
protected set
{{
this.{property.Name}.Clear();
foreach (var item in value)
{{
this.{property.Name}.Add(item);
}}
}}
}}";
}
private string CreateIdPropertyIfRequired(Type type)
{
if (type.GetProperty("Id") is null)
{
return @"
/// <summary>
/// Gets or sets the identifier of this instance.
/// </summary>
public Guid Id { get; set; }";
}
return string.Empty;
}
private IEnumerable<PropertyInfo> GetStandaloneCollectionProperties(Type type)
{
return type.FullName != GameConfigurationFullName ?
type.GetProperties().Where(p => p.PropertyType.IsGenericType
&& p.PropertyType.GetGenericTypeDefinition() == typeof(ICollection<>)
&& !IsMemberOfAggregate(p)
&& IsStandaloneType(p.PropertyType.GenericTypeArguments[0].FullName, type)).ToList() :
Enumerable.Empty<PropertyInfo>();
}
private string CreateConstructors(Type type, bool requiresJoinCollections)
{
var stringBuilder = new StringBuilder();
var className = type.Name;
if (requiresJoinCollections
|| (type.GetConstructors().Any(c => c.IsPublic && c.GetParameters().Length > 0)
&& type.GetConstructors().Any(c => c.GetParameters().Length == 0)))
{
stringBuilder.AppendLine(@$"/// <inheritdoc />
public {className}()
{{
{(requiresJoinCollections ? " this.InitJoinCollections();" : null)}
}}");
}
foreach (var constructor in type.GetConstructors()
.Where(c => c.IsPublic && c.GetParameters().Length > 0))
{
var parameters = constructor.GetParameters();
stringBuilder.Append(@$"
/// <inheritdoc />
public {className}({ModelGeneratorHelper.GetParameterDefinitions(parameters)})
: base({ModelGeneratorHelper.GetParameters(parameters)})
{{
{(requiresJoinCollections ? " this.InitJoinCollections();" : null)}
}}
");
}
return stringBuilder.ToString();
}
}

View File

@@ -0,0 +1,17 @@
// <copyright file="IUnboundSourceGenerator.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Persistence.SourceGenerator;
/// <summary>
/// Interface for a generator which generates sources without depending on a context.
/// </summary>
public interface IUnboundSourceGenerator
{
/// <summary>
/// Generates the source files.
/// </summary>
/// <returns>The created source files.</returns>
public IEnumerable<(string Name, string Source)> GenerateSources();
}

View File

@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<Nullable>warnings</Nullable>
<WarningsAsErrors>nullable;CS4014;VSTHRD110;VSTHRD100</WarningsAsErrors>
<DocumentationFile>bin\$(Configuration)\MUnique.OpenMU.Persistence.SourceGenerator.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Annotations\MUnique.OpenMU.Annotations.csproj" />
<ProjectReference Include="..\..\DataModel\MUnique.OpenMU.DataModel.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,166 @@
// <copyright file="ModelGeneratorHelper.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Persistence.SourceGenerator;
using System.Reflection;
using MUnique.OpenMU.DataModel;
/// <summary>
/// Helper class containing shared functionality for model generators.
/// </summary>
internal static class ModelGeneratorHelper
{
private static IList<Type> _customTypes;
/// <summary>
/// Gets a header template for a generated file.
/// </summary>
public static string FileHeaderTemplate => @"// <copyright file=""{0}.Generated.cs"" company=""MUnique"">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
//------------------------------------------------------------------------------
// <auto-generated>
// This source code was auto-generated by a roslyn code generator.
// </auto-generated>
//------------------------------------------------------------------------------
// ReSharper disable All";
/// <summary>
/// Gets the namespace of the configuration classes.
/// </summary>
public static string ConfigurationNamespace => "MUnique.OpenMU.DataModel.Configuration";
/// <summary>
/// Gets the types which need to be customized for persistence.
/// </summary>
public static IEnumerable<Type> CustomTypes => _customTypes ??= GetCustomTypes();
/// <summary>
/// Determines whether the given type is a configuration type.
/// </summary>
/// <param name="type">The type.</param>
/// <returns><c>true</c> if the given type is a configuration type; otherwise, <c>false</c>.</returns>
public static bool IsConfigurationType(Type type)
{
if (type.Namespace != null
&& type.Namespace.StartsWith(ConfigurationNamespace, StringComparison.InvariantCulture))
{
return true;
}
if (type.BaseType is { Namespace: { } }
&& type.BaseType.Namespace.StartsWith(ConfigurationNamespace, StringComparison.InvariantCulture))
{
return true;
}
if (type.Name.Contains("Definition", StringComparison.InvariantCulture))
{
return true;
}
if (type.Name is "AttributeRelationship" or "PlugInConfiguration" or "ConstValueAttribute")
{
return true;
}
return false;
}
/// <summary>
/// Gets the parameter definitions for a method or constructor.
/// </summary>
/// <param name="parameters">The parameters.</param>
/// <returns>The string of the parameter definitions.</returns>
public static string GetParameterDefinitions(ICollection<ParameterInfo> parameters)
{
var result = new StringBuilder();
foreach (var p in parameters)
{
result.Append(p.ParameterType.GetCSharpFullName())
.Append(" ")
.Append(p.Name);
if (parameters.Count > p.Position + 1)
{
result.Append(", ");
}
}
return result.ToString();
}
/// <summary>
/// Gets the parameters used to call a method.
/// </summary>
/// <param name="parameters">The parameter infos.</param>
/// <returns>The parameters used to call a method.</returns>
public static string GetParameters(ICollection<ParameterInfo> parameters)
{
var result = new StringBuilder();
foreach (var p in parameters)
{
result.Append(p.Name);
if (parameters.Count > p.Position + 1)
{
result.Append(", ");
}
}
return result.ToString();
}
/// <summary>
/// Overrides the <see cref="ICloneable{T}"/> implementation, so that the correct class instance
/// is created and the id is assigned.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="className">Name of the class.</param>
/// <returns>The implementation for <see cref="ICloneable{T}"/>.</returns>
public static string OverrideClonable(Type type, string className)
{
return $$"""
/// <inheritdoc />
public override {{type.Namespace}}.{{className}} Clone(MUnique.OpenMU.DataModel.Configuration.GameConfiguration gameConfiguration)
{
var clone = new {{className}}();
clone.AssignValuesOf(this, gameConfiguration);
return clone;
}
/// <inheritdoc />
public override void AssignValuesOf({{type.Namespace}}.{{className}} other, MUnique.OpenMU.DataModel.Configuration.GameConfiguration gameConfiguration)
{
base.AssignValuesOf(other, gameConfiguration);
this.Id = other.GetId();
}
""";
}
/// <summary>
/// Determines the types which require customization.
/// </summary>
/// <returns>The types that require customization.</returns>
private static List<Type> GetCustomTypes()
{
var result = new List<Type>();
var loadedTypes = typeof(DataModel.Attributes.PowerUpDefinition).Assembly.GetTypes()
.Where(type => type.IsClass && type.IsPublic)
.Where(type => !type.IsSealed && !type.IsAbstract && type.GetConstructor([]) != null).ToList();
result.AddRange(loadedTypes);
result.Add(typeof(MUnique.OpenMU.AttributeSystem.AttributeDefinition));
result.Add(typeof(MUnique.OpenMU.AttributeSystem.StatAttribute));
result.Add(typeof(MUnique.OpenMU.AttributeSystem.ConstValueAttribute));
result.Add(typeof(MUnique.OpenMU.AttributeSystem.AttributeRelationship));
result.Add(typeof(MUnique.OpenMU.Interfaces.LetterHeader));
result.Add(typeof(MUnique.OpenMU.Interfaces.Friend));
result.Add(typeof(MUnique.OpenMU.PlugIns.PlugInConfiguration));
return result;
}
}

View File

@@ -0,0 +1,64 @@
// <copyright file="Program.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Persistence.SourceGenerator;
using System.IO;
/// <summary>
/// Main entry point for the generator, if it's used as an executable.
/// </summary>
public static class Program
{
/// <summary>
/// The main entry point function.
/// It expects two arguments:
/// - The target assembly name
/// - The target folder path for the generated code.
/// </summary>
/// <param name="args">The arguments.</param>
public static int Main(params string[] args)
{
Console.WriteLine("Started generator with these parameters:");
foreach (var arg in args)
{
Console.WriteLine(arg);
}
if (args.Length < 2)
{
Console.WriteLine("Can't generate code. Please add the project name and the target folder path as starting parameter.");
return 1;
}
IUnboundSourceGenerator generator = args[0] switch
{
EfCoreModelGenerator.TargetAssemblyName => new EfCoreModelGenerator(),
BasicModelGenerator.TargetAssemblyName => new BasicModelGenerator(),
_ => null,
};
if (generator is null)
{
Console.WriteLine($"No generator found for target assembly '{args[0]}'.");
return 2;
}
var targetFolder = args[1];
var previouslyGeneratedFile = Directory.EnumerateFiles(targetFolder, ".Generated.cs");
foreach (var file in previouslyGeneratedFile)
{
File.Delete(file);
}
foreach (var (name, source) in generator.GenerateSources())
{
var filePath = Path.Combine(targetFolder, name + ".Generated.cs");
Console.WriteLine($"Writing {filePath}");
File.WriteAllText(filePath, source);
}
return 0;
}
}

View File

@@ -0,0 +1,10 @@
// <copyright file="AssemblyInfo.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
using System.Reflection;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("MUnique.OpenMU.Persistence.SourceGenerator")]

View File

@@ -0,0 +1,21 @@
// <copyright file="StringExtensions.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Persistence.SourceGenerator;
/// <summary>
/// Extension methods for strings.
/// </summary>
public static class StringExtensions
{
/// <summary>
/// Converts the name to camel case.
/// </summary>
/// <param name="name">The name which should be converted.</param>
/// <returns>The converted name in camel case.</returns>
internal static string ToCamelCase(this string name)
{
return name.Substring(0, 1).ToLowerInvariant() + name.Substring(1);
}
}

View File

@@ -0,0 +1,41 @@
// <copyright file="TypeExtensions.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Persistence.SourceGenerator;
/// <summary>
/// Extension methods for <see cref="Type"/>s.
/// </summary>
public static class TypeExtensions
{
/// <summary>
/// Gets the printable C# full name of the type.
/// </summary>
/// <param name="type">The type whose name is requested.</param>
/// <returns>The printable C# full name of the type.</returns>
internal static string GetCSharpFullName(this Type type)
{
if (!type.IsGenericType)
{
return type.FullName ?? type.Name;
}
return type.Name.Split('`')[0] + "<" + string.Join(", ", type.GetGenericArguments().Select(x => x.FullName).ToArray()) + ">";
}
/// <summary>
/// Gets the printable C#-name of the type.
/// </summary>
/// <param name="type">The type whose name is requested.</param>
/// <returns>The printable C#-name of the type.</returns>
internal static string GetCSharpName(this Type type)
{
if (!type.IsGenericType)
{
return type.Name;
}
return type.Name.Split('`')[0] + "<" + string.Join(", ", type.GetGenericArguments().Select(x => x.Name).ToArray()) + ">";
}
}