diff --git a/AUTODISPOSE.md b/AUTODISPOSE.md
new file mode 100644
index 0000000..4b16bd9
--- /dev/null
+++ b/AUTODISPOSE.md
@@ -0,0 +1,42 @@
+# AutoDispose Source Generator
+
+This feature implements automatic dispose code generation for classes inheriting from `DisposableBase`.
+
+## Usage
+
+1. Apply the `[AutoDispose]` attribute to your class
+2. Make the class `partial`
+3. The source generator will automatically create a `Dispose(bool manual, bool wasDisposed)` method that:
+ - Finds all fields implementing `IDisposable` or `Platform.Disposables.IDisposable`
+ - Calls `Dispose()` on each field using null-conditional operator (`?.`)
+ - Sets non-readonly fields to `null` after disposal
+
+## Example
+
+```csharp
+[AutoDispose]
+public partial class MyClass : DisposableBase
+{
+ private readonly FileStream _readOnlyField;
+ private MemoryStream _writableField;
+
+ // Generated Dispose method will be:
+ // protected override void Dispose(bool manual, bool wasDisposed)
+ // {
+ // if (!wasDisposed)
+ // {
+ // _readOnlyField?.Dispose();
+ // _writableField?.Dispose();
+ // _writableField = null;
+ // }
+ // }
+}
+```
+
+## Benefits
+
+- Reduces boilerplate code
+- Prevents memory leaks from forgotten disposals
+- Automatically handles null checks
+- Respects readonly field constraints
+- Compile-time code generation for zero runtime overhead
\ No newline at end of file
diff --git a/csharp/Platform.Disposables.SourceGenerator/AutoDisposeSourceGenerator.cs b/csharp/Platform.Disposables.SourceGenerator/AutoDisposeSourceGenerator.cs
new file mode 100644
index 0000000..9619f9d
--- /dev/null
+++ b/csharp/Platform.Disposables.SourceGenerator/AutoDisposeSourceGenerator.cs
@@ -0,0 +1,173 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Text;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Platform.Disposables.SourceGenerator
+{
+ [Generator]
+ public class AutoDisposeSourceGenerator : ISourceGenerator
+ {
+ public void Initialize(GeneratorInitializationContext context)
+ {
+ context.RegisterForSyntaxNotifications(() => new ClassSyntaxReceiver());
+ }
+
+ public void Execute(GeneratorExecutionContext context)
+ {
+ if (!(context.SyntaxReceiver is ClassSyntaxReceiver receiver))
+ return;
+
+ foreach (var classDeclaration in receiver.CandidateClasses)
+ {
+ var semanticModel = context.Compilation.GetSemanticModel(classDeclaration.SyntaxTree);
+ var classSymbol = semanticModel.GetDeclaredSymbol(classDeclaration);
+
+ if (classSymbol == null)
+ continue;
+
+ if (!InheritsFromDisposableBase(classSymbol))
+ continue;
+
+ if (HasAutoDisposeAttribute(classSymbol))
+ {
+ var sourceCode = GenerateDisposeMethod(classSymbol, classDeclaration);
+ if (!string.IsNullOrEmpty(sourceCode))
+ {
+ context.AddSource($"{classSymbol.Name}.AutoDispose.g.cs", SourceText.From(sourceCode, Encoding.UTF8));
+ }
+ }
+ }
+ }
+
+ private bool InheritsFromDisposableBase(INamedTypeSymbol classSymbol)
+ {
+ var baseType = classSymbol.BaseType;
+ while (baseType != null)
+ {
+ if (baseType.Name == "DisposableBase" &&
+ baseType.ContainingNamespace?.ToDisplayString() == "Platform.Disposables")
+ {
+ return true;
+ }
+ baseType = baseType.BaseType;
+ }
+ return false;
+ }
+
+ private bool HasAutoDisposeAttribute(INamedTypeSymbol classSymbol)
+ {
+ return classSymbol.GetAttributes().Any(a =>
+ a.AttributeClass?.Name == "AutoDisposeAttribute" ||
+ a.AttributeClass?.Name == "AutoDispose");
+ }
+
+ private string GenerateDisposeMethod(INamedTypeSymbol classSymbol, ClassDeclarationSyntax classDeclaration)
+ {
+ var disposableFields = GetDisposableFields(classSymbol);
+ if (!disposableFields.Any())
+ return string.Empty;
+
+ var namespaceName = classSymbol.ContainingNamespace.ToDisplayString();
+ var className = classSymbol.Name;
+
+ var disposeStatements = GenerateDisposeStatements(disposableFields);
+
+ var sourceCode = $@"//
+#nullable enable
+using System;
+
+namespace {namespaceName}
+{{
+ partial class {className}
+ {{
+ protected override void Dispose(bool manual, bool wasDisposed)
+ {{
+ if (!wasDisposed)
+ {{
+{disposeStatements}
+ }}
+ }}
+ }}
+}}";
+
+ return sourceCode;
+ }
+
+ private List GetDisposableFields(INamedTypeSymbol classSymbol)
+ {
+ var disposableFields = new List();
+
+ foreach (var member in classSymbol.GetMembers())
+ {
+ if (member is IFieldSymbol field)
+ {
+ if (IsDisposableType(field.Type))
+ {
+ disposableFields.Add(field);
+ }
+ }
+ }
+
+ return disposableFields;
+ }
+
+ private bool IsDisposableType(ITypeSymbol typeSymbol)
+ {
+ // Check for System.IDisposable
+ if (ImplementsInterface(typeSymbol, "System", "IDisposable"))
+ return true;
+
+ // Check for Platform.Disposables.IDisposable
+ if (ImplementsInterface(typeSymbol, "Platform.Disposables", "IDisposable"))
+ return true;
+
+ return false;
+ }
+
+ private bool ImplementsInterface(ITypeSymbol typeSymbol, string namespaceName, string interfaceName)
+ {
+ return typeSymbol.AllInterfaces.Any(i =>
+ i.Name == interfaceName &&
+ i.ContainingNamespace?.ToDisplayString() == namespaceName);
+ }
+
+ private string GenerateDisposeStatements(List disposableFields)
+ {
+ var statements = new StringBuilder();
+
+ foreach (var field in disposableFields)
+ {
+ statements.AppendLine($" {field.Name}?.Dispose();");
+ // Only set to null if field is not readonly
+ if (field.Type.CanBeReferencedByName && !field.IsReadOnly)
+ {
+ statements.AppendLine($" {field.Name} = null;");
+ }
+ }
+
+ return statements.ToString().TrimEnd();
+ }
+ }
+
+ internal class ClassSyntaxReceiver : ISyntaxReceiver
+ {
+ public List CandidateClasses { get; } = new();
+
+ public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
+ {
+ if (syntaxNode is ClassDeclarationSyntax classDeclaration)
+ {
+ // Look for classes that might need auto-dispose generation
+ if (classDeclaration.AttributeLists.Any() ||
+ classDeclaration.BaseList?.Types.Any() == true)
+ {
+ CandidateClasses.Add(classDeclaration);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/csharp/Platform.Disposables.SourceGenerator/Platform.Disposables.SourceGenerator.csproj b/csharp/Platform.Disposables.SourceGenerator/Platform.Disposables.SourceGenerator.csproj
new file mode 100644
index 0000000..52d15ea
--- /dev/null
+++ b/csharp/Platform.Disposables.SourceGenerator/Platform.Disposables.SourceGenerator.csproj
@@ -0,0 +1,18 @@
+
+
+
+ netstandard2.0
+ latest
+ enable
+ false
+ false
+ true
+ true
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/csharp/Platform.Disposables.Tests.SourceGenerator/AutoDisposeTests.cs b/csharp/Platform.Disposables.Tests.SourceGenerator/AutoDisposeTests.cs
new file mode 100644
index 0000000..e962cf2
--- /dev/null
+++ b/csharp/Platform.Disposables.Tests.SourceGenerator/AutoDisposeTests.cs
@@ -0,0 +1,72 @@
+using System;
+using System.IO;
+using Xunit;
+
+namespace Platform.Disposables.Tests.SourceGenerator
+{
+ public class AutoDisposeTests
+ {
+ [Fact]
+ public void AutoDispose_DisposesAllFields_WhenObjectIsDisposed()
+ {
+ var testDisposable = new TestAutoDispose();
+
+ Assert.False(testDisposable.IsDisposed);
+ Assert.False(testDisposable.DisposableField1.IsDisposed);
+ Assert.False(testDisposable.DisposableField2.IsDisposed);
+ Assert.False(testDisposable.SystemDisposableField.WasDisposed);
+
+ testDisposable.Dispose();
+
+ Assert.True(testDisposable.IsDisposed);
+ Assert.True(testDisposable.DisposableField1.IsDisposed);
+ Assert.True(testDisposable.DisposableField2.IsDisposed);
+ Assert.True(testDisposable.SystemDisposableField.WasDisposed);
+ }
+
+ [Fact]
+ public void AutoDispose_HandlesNullFields_Gracefully()
+ {
+ var testDisposable = new TestAutoDisposeWithNulls();
+
+ Assert.False(testDisposable.IsDisposed);
+
+ // Should not throw when disposing null fields
+ testDisposable.Dispose();
+
+ Assert.True(testDisposable.IsDisposed);
+ }
+ }
+
+ [AutoDispose]
+ public partial class TestAutoDispose : DisposableBase
+ {
+ public readonly Disposable DisposableField1;
+ public readonly Disposable DisposableField2;
+ public readonly TestSystemDisposable SystemDisposableField;
+
+ public TestAutoDispose()
+ {
+ DisposableField1 = new Disposable();
+ DisposableField2 = new Disposable();
+ SystemDisposableField = new TestSystemDisposable();
+ }
+ }
+
+ [AutoDispose]
+ public partial class TestAutoDisposeWithNulls : DisposableBase
+ {
+ public readonly Disposable? DisposableField1 = null;
+ public readonly TestSystemDisposable? SystemDisposableField = null;
+ }
+
+ public class TestSystemDisposable : System.IDisposable
+ {
+ public bool WasDisposed { get; private set; }
+
+ public void Dispose()
+ {
+ WasDisposed = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/csharp/Platform.Disposables.Tests.SourceGenerator/Platform.Disposables.Tests.SourceGenerator.csproj b/csharp/Platform.Disposables.Tests.SourceGenerator/Platform.Disposables.Tests.SourceGenerator.csproj
new file mode 100644
index 0000000..601d2e3
--- /dev/null
+++ b/csharp/Platform.Disposables.Tests.SourceGenerator/Platform.Disposables.Tests.SourceGenerator.csproj
@@ -0,0 +1,24 @@
+
+
+
+ net8
+ latest
+ enable
+ false
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/csharp/Platform.Disposables.Tests/DisposableTests.cs b/csharp/Platform.Disposables.Tests/DisposableTests.cs
index caa19fb..51867ea 100644
--- a/csharp/Platform.Disposables.Tests/DisposableTests.cs
+++ b/csharp/Platform.Disposables.Tests/DisposableTests.cs
@@ -42,7 +42,7 @@ private static ProcessStartInfo CreateProcessStartInfo(string logPath, bool wait
return new ProcessStartInfo
{
FileName = "dotnet",
- Arguments = $"run -p \"{projectPath}\" -f net7 \"{logPath}\" {waitForCancellation.ToString()}",
+ Arguments = $"run -p \"{projectPath}\" -f net8 \"{logPath}\" {waitForCancellation.ToString()}",
UseShellExecute = false,
CreateNoWindow = true
};
diff --git a/csharp/Platform.Disposables/AutoDisposeAttribute.cs b/csharp/Platform.Disposables/AutoDisposeAttribute.cs
new file mode 100644
index 0000000..beae4f5
--- /dev/null
+++ b/csharp/Platform.Disposables/AutoDisposeAttribute.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace Platform.Disposables
+{
+ /// <summary>
+ /// <para>Indicates that the class should have automatic dispose code generation for all disposable fields.</para>
+ /// <para>Указывает, что для класса должен быть автоматически сгенерирован код для высвобождения всех высвобождаемых полей.</para>
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
+ public sealed class AutoDisposeAttribute : Attribute
+ {
+ }
+}
\ No newline at end of file
diff --git a/csharp/Platform.Disposables/Platform.Disposables.csproj b/csharp/Platform.Disposables/Platform.Disposables.csproj
index 957fb95..46be3f0 100644
--- a/csharp/Platform.Disposables/Platform.Disposables.csproj
+++ b/csharp/Platform.Disposables/Platform.Disposables.csproj
@@ -4,7 +4,7 @@
LinksPlatform's Platform.Disposables Class Library
Konstantin Diachenko
Platform.Disposables
- 0.4.0
+ 0.5.0
Konstantin Diachenko
net8
Platform.Disposables
@@ -23,7 +23,7 @@
true
snupkg
latest
- Update target framework from net7 to net8.
+ Add AutoDispose source generator for automatic dispose code generation.
enable
@@ -39,6 +39,10 @@
+
+
+
+