Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ The same package bundles Roslyn tooling: an incremental source generator that em
* Never break the public surface. Deprecate a renamed member with `[Obsolete]` with a JetBrains `[CodeTemplate]` auto-conversion; the obsolete member forwards to the replacement and never throws or changes behavior.
* Mark a member Revit invokes but consumers must not call `[EditorBrowsable(EditorBrowsableState.Never)]`.
* The Roslyn tooling is public surface. It targets `netstandard2.0`, dual-targets Roslyn (the default with the `.Roslyn414` twins), uses stable `RVTTK####` diagnostic ids, and needs an `AnalyzerReleases.Unshipped.md` entry per new or changed rule. Never rename a generated member or renumber a shipped id.
* The `AddInAssemblyGenerator` runs in every project with project references. Its output compiles with C# 7.3 and declares its own attribute type. The runtime matches `Nice3point.Revit.Toolkit.Generated.AddInAssemblyAttribute` by full name: never rename it.
* The MSBuild targets in `source/Nice3point.Revit.Toolkit/build` are public surface. Their targets, items, and properties take the `_RevitToolkit` prefix.
* Every type compiles under every supported configuration.
* A change ships with a test covering the toolkit's custom logic: Revit behavior runs on the Revit thread, and the analyzers, fixers, and generator run against the Roslyn testing harness.
* Confirm an unfamiliar Revit or .NET API before use through official docs or `gh` (`gh api`, `gh search code`).
* A public-surface change updates `README.md`, `CHANGELOG.md`, and the XML docs in the same commit.

## Repository map

* `source/Nice3point.Revit.Toolkit` it packs the runtime library and the tooling into one NuGet package for users.
* `source/Nice3point.Revit.Toolkit` it packs the runtime library and the tooling into one NuGet package for users. Its `build/` folder contains the MSBuild targets NuGet imports into a consumer project.
* `source/Nice3point.Revit.Toolkit.Analyzers`, `Nice3point.Revit.Toolkit.Analyzers.CodeFixers`, and `Nice3point.Revit.Toolkit.SourceGenerators` hold the analyzers, code fixers, and source generator, each with a `.Roslyn414` twin for the older Roslyn.
* `tests/` — `Nice3point.Revit.Toolkit.Tests` runs inside a Revit process; the analyzer, fixer, and generator test projects run against the compiler.
* `build/` — the ModularPipelines build.
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
# 2027.0.4

### External Events

- External events always run as your add-in, even when created on a background thread or in a modeless window. Inside the handler, `ActiveAddInId` returns your add-in, and Extensible Storage schemas with vendor access accept the changes. Previously, Revit ran such a handler without an add-in.
- `ExternalApplication` and `ExternalCommand` set this up automatically, for the add-in project and every project it references.
- New `RegisterAddInAssembly()` method for the remaining cases: an `IExternalApplication` without the Toolkit base class, an assembly loaded at run time, or a library from a NuGet package. Call it in `OnStartup`:

```c#
public Result OnStartup(UIControlledApplication application)
{
application.RegisterAddInAssembly(typeof(Application).Assembly);
return Result.Succeeded;
}
```

# 2027.0.3

### External Events
Expand Down
16 changes: 8 additions & 8 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@
<PackageVersion Include="Nice3point.Revit.Extensions" Version="2026.1.5" Condition="$(RevitVersion) == '2026'"/>
<PackageVersion Include="Nice3point.Revit.Extensions" Version="2027.0.5" Condition="$(RevitVersion) == '2027'"/>

<PackageVersion Include="Nice3point.TUnit.Revit" Version="2021.1.7" Condition="$(RevitVersion) == '2021'"/>
<PackageVersion Include="Nice3point.TUnit.Revit" Version="2022.1.7" Condition="$(RevitVersion) == '2022'"/>
<PackageVersion Include="Nice3point.TUnit.Revit" Version="2023.1.7" Condition="$(RevitVersion) == '2023'"/>
<PackageVersion Include="Nice3point.TUnit.Revit" Version="2024.1.7" Condition="$(RevitVersion) == '2024'"/>
<PackageVersion Include="Nice3point.TUnit.Revit" Version="2025.1.7" Condition="$(RevitVersion) == '2025'"/>
<PackageVersion Include="Nice3point.TUnit.Revit" Version="2026.1.7" Condition="$(RevitVersion) == '2026'"/>
<PackageVersion Include="Nice3point.TUnit.Revit" Version="2027.0.7" Condition="$(RevitVersion) == '2027'"/>
<PackageVersion Include="Nice3point.TUnit.Revit" Version="2021.1.8" Condition="$(RevitVersion) == '2021'"/>
<PackageVersion Include="Nice3point.TUnit.Revit" Version="2022.1.8" Condition="$(RevitVersion) == '2022'"/>
<PackageVersion Include="Nice3point.TUnit.Revit" Version="2023.1.8" Condition="$(RevitVersion) == '2023'"/>
<PackageVersion Include="Nice3point.TUnit.Revit" Version="2024.1.8" Condition="$(RevitVersion) == '2024'"/>
<PackageVersion Include="Nice3point.TUnit.Revit" Version="2025.1.8" Condition="$(RevitVersion) == '2025'"/>
<PackageVersion Include="Nice3point.TUnit.Revit" Version="2026.1.8" Condition="$(RevitVersion) == '2026'"/>
<PackageVersion Include="Nice3point.TUnit.Revit" Version="2027.0.8" Condition="$(RevitVersion) == '2027'"/>
</ItemGroup>

<!-- Core dependencies -->
Expand All @@ -44,7 +44,7 @@

<!-- Testing -->
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8"/>
<PackageVersion Include="TUnit" Version="1.73.19"/>
<PackageVersion Include="TUnit" Version="1.76.45"/>
<PackageVersion Include="Verify.TUnit" Version="33.3.3"/>
<PackageVersion Include="Verify.SourceGenerators" Version="2.5.0"/>
<PackageVersion Include="Shouldly" Version="4.3.0"/>
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,15 @@ A raise that Revit refuses with `Denied` or `TimedOut` never runs.
`Raise(argument)` returns that result to the caller.
The `RaiseAsync()` task fails with an `InvalidOperationException`.

Inside the event handler, `UIApplication.ActiveAddInId` returns the ID of your add-in, even when the event is created on a background thread or in a modeless window.
Extensible Storage schemas with vendor write access accept the changes of the handler.
`ExternalApplication` and `ExternalCommand` set this up automatically.
Without them, or for a library from a NuGet package or loaded at run time, register the assembly in `OnStartup`:

```c#
application.RegisterAddInAssembly(typeof(Application).Assembly);
```

#### ExternalEvent

A synchronous external event that queues the handler via the Revit external event mechanism.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System.Text;
using Microsoft.CodeAnalysis.CSharp;
using Nice3point.Revit.Toolkit.SourceGenerators.Models;

namespace Nice3point.Revit.Toolkit.SourceGenerators.AddInAssemblies;

/// <summary>
/// Writes the assembly attributes and the internal attribute type for the listed project references.
/// </summary>
/// <remarks>
/// Each assembly declares its own attribute type.
/// The attributes resolve whichever toolkit version Revit loads into the shared context.
/// The source compiles with C# 7.3, the default language version of a .NET Framework project.
/// </remarks>
internal static class AddInAssemblyWriter
{
public const string HintName = "AddInAssemblies.g.cs";

private const string GeneratorName = "Nice3point.Revit.Toolkit.SourceGenerators.AddInAssemblyGenerator";
private const string AttributeNamespace = "Nice3point.Revit.Toolkit.Generated";
private const string AttributeName = "AddInAssemblyAttribute";

private static readonly string AssemblyVersion = typeof(AddInAssemblyGenerator).Assembly.GetName().Version?.ToString() ?? "1.0.0";

public static string GenerateSource(EquatableArray<string> assemblyNames)
{
var builder = new StringBuilder();
builder.AppendLine("// <auto-generated/>");
builder.AppendLine("#pragma warning disable");
builder.AppendLine();

foreach (var assemblyName in assemblyNames)
{
builder.AppendLine($"[assembly: global::{AttributeNamespace}.{AttributeName}({SymbolDisplay.FormatLiteral(assemblyName, quote: true)})]");
}

builder.AppendLine($$"""

namespace {{AttributeNamespace}}
{
[global::System.CodeDom.Compiler.GeneratedCode("{{GeneratorName}}", "{{AssemblyVersion}}")]
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
[global::System.AttributeUsage(global::System.AttributeTargets.Assembly, AllowMultiple = true)]
internal sealed class {{AttributeName}} : global::System.Attribute
{
public {{AttributeName}}(string assemblyName)
{
AssemblyName = assemblyName;
}

public string AssemblyName { get; }
}
}
""");

return builder.ToString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis.Diagnostics;
using Nice3point.Revit.Toolkit.SourceGenerators.Models;

namespace Nice3point.Revit.Toolkit.SourceGenerators.AddInAssemblies;

/// <summary>
/// Reads the project references listed by the package targets.
/// </summary>
internal static class AddInProjectReferences
{
private const string PropertyName = "build_property._RevitToolkitAddInProjectReferences";

/// <summary>
/// Reads the assembly names of the project references from the global analyzer config options.
/// </summary>
/// <param name="globalOptions">The global analyzer config options of the compilation.</param>
/// <returns>The distinct assembly names in ordinal order, or an empty array for a project without project references.</returns>
public static EquatableArray<string> Read(AnalyzerConfigOptions globalOptions)
{
if (!globalOptions.TryGetValue(PropertyName, out var value))
{
return ImmutableArray<string>.Empty;
}

return value.Split(',')
.Select(static assemblyName => assemblyName.Trim())
.Where(static assemblyName => assemblyName.Length > 0)
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(static assemblyName => assemblyName, StringComparer.Ordinal)
.ToImmutableArray();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using Nice3point.Revit.Toolkit.SourceGenerators.AddInAssemblies;

namespace Nice3point.Revit.Toolkit.SourceGenerators;

/// <summary>
/// Incremental source generator that declares an <c>[assembly: AddInAssembly]</c> attribute for each project reference.
/// </summary>
/// <remarks>
/// The package targets list the project references in a build property, transitive ones included.
/// <para>A project without project references generates no source.</para>
/// </remarks>
[Generator(LanguageNames.CSharp)]
public sealed class AddInAssemblyGenerator : IIncrementalGenerator
{
/// <inheritdoc />
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var assemblyNames = context.AnalyzerConfigOptionsProvider
.Select(static (optionsProvider, _) => AddInProjectReferences.Read(optionsProvider.GlobalOptions));

context.RegisterSourceOutput(assemblyNames, static (sourceProductionContext, projectReferences) =>
{
if (projectReferences.IsEmpty)
{
return;
}

var generatedSource = AddInAssemblyWriter.GenerateSource(projectReferences);
sourceProductionContext.AddSource(AddInAssemblyWriter.HintName, SourceText.From(generatedSource, Encoding.UTF8));
});
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.ComponentModel;
using Autodesk.Revit.UI;
using Nice3point.Revit.Toolkit.Helpers;
using Nice3point.Revit.Toolkit.Internal;
#if NET
using System.Runtime.Loader;
#endif
Expand Down Expand Up @@ -46,7 +47,9 @@ public Result OnStartup(UIControlledApplication application)
Application = application;

var currentType = GetType();
AddInContext.Capture(currentType.Assembly);
#if NET

if (AssemblyLoadContext.GetLoadContext(currentType.Assembly) == AssemblyLoadContext.Default)
{
using (ResolveHelper.BeginAssemblyResolveScope(currentType))
Expand All @@ -59,6 +62,7 @@ public Result OnStartup(UIControlledApplication application)
OnStartup();
}
#else

using (ResolveHelper.BeginAssemblyResolveScope(currentType))
{
OnStartup();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.ComponentModel;
using Autodesk.Revit.UI;
using Nice3point.Revit.Toolkit.Helpers;
using Nice3point.Revit.Toolkit.Internal;
#if NET
using System.Runtime.Loader;
#endif
Expand Down Expand Up @@ -129,7 +130,9 @@ public Result Execute(ExternalCommandData commandData, ref string message, Eleme
JournalData = commandData.JournalData;

var currentType = GetType();
AddInContext.Capture(currentType.Assembly);
#if NET

if (AssemblyLoadContext.GetLoadContext(currentType.Assembly) == AssemblyLoadContext.Default)
{
using (ResolveHelper.BeginAssemblyResolveScope(currentType))
Expand All @@ -142,6 +145,7 @@ public Result Execute(ExternalCommandData commandData, ref string message, Eleme
Execute();
}
#else

using (ResolveHelper.BeginAssemblyResolveScope(currentType))
{
Execute();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System.Reflection;
using Autodesk.Revit.UI;
using Nice3point.Revit.Toolkit.Internal;

// ReSharper disable once CheckNamespace
namespace Nice3point.Revit.Toolkit.External;

/// <summary>
/// Provides extension methods for <see cref="UIControlledApplication" /> and <see cref="UIApplication" /> to register add-in assemblies.
/// </summary>
[PublicAPI]
public static class AddInAssemblyExtensions
{
/// <summary>
/// Defines extensions for <see cref="UIControlledApplication" />.
/// </summary>
/// <param name="application">The application passed to the add-in at startup.</param>
extension(UIControlledApplication application)
{
/// <summary>
/// Registers the assembly as part of the active add-in.
/// </summary>
/// <param name="assembly">The assembly with external event handlers of the add-in.</param>
/// <exception cref="T:System.ArgumentNullException"><paramref name="assembly" /> is <see langword="null" />.</exception>
/// <exception cref="T:System.InvalidOperationException">The method is called outside a Revit callback.</exception>
/// <remarks>
/// External events of the assembly run as the active add-in on any thread.
/// Project references of the assembly are registered too.
/// <see cref="ExternalApplication" /> and <see cref="ExternalCommand" /> register their assembly automatically.
/// An external event created before the registration runs as the add-in Revit bound it to at creation.
/// On a Revit version without the expected native API, the method does nothing.
/// </remarks>
public void RegisterAddInAssembly(Assembly assembly)
{
ArgumentNullException.ThrowIfNull(assembly);
AddInContext.Register(assembly);
}
}

/// <summary>
/// Defines extensions for <see cref="UIApplication" />.
/// </summary>
/// <param name="application">The application passed to a command or an event handler.</param>
extension(UIApplication application)
{
/// <inheritdoc cref="AddInAssemblyExtensions.RegisterAddInAssembly(UIControlledApplication, Assembly)" />
public void RegisterAddInAssembly(Assembly assembly)
{
ArgumentNullException.ThrowIfNull(assembly);
AddInContext.Register(assembly);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public AsyncExternalEvent(Action handler) : this(handler, ExternalEventOptions.N
/// <param name="handler">The execution logic.</param>
/// <param name="options">The options to use to configure the external event.</param>
/// <exception cref="System.ArgumentNullException">Thrown if <paramref name="handler" /> is <see langword="null" />.</exception>
public AsyncExternalEvent(Action handler, ExternalEventOptions options)
public AsyncExternalEvent(Action handler, ExternalEventOptions options) : base(handler)
{
ArgumentNullException.ThrowIfNull(handler);

Expand Down Expand Up @@ -56,7 +56,7 @@ public AsyncExternalEvent(Action<UIApplication> handler) : this(handler, Externa
/// <param name="handler">The execution logic that receives the current <see cref="UIApplication" />.</param>
/// <param name="options">The options to use to configure the external event.</param>
/// <exception cref="System.ArgumentNullException">Thrown if <paramref name="handler" /> is <see langword="null" />.</exception>
public AsyncExternalEvent(Action<UIApplication> handler, ExternalEventOptions options)
public AsyncExternalEvent(Action<UIApplication> handler, ExternalEventOptions options) : base(handler)
{
ArgumentNullException.ThrowIfNull(handler);

Expand Down Expand Up @@ -84,7 +84,7 @@ public AsyncExternalEvent(Action<UIApplication, CancellationToken> handler) : th
/// <param name="handler">The execution logic that receives the current <see cref="UIApplication" /> and the token passed to <see cref="RaiseAsync(CancellationToken)" />.</param>
/// <param name="options">The options to use to configure the external event.</param>
/// <exception cref="System.ArgumentNullException">Thrown if <paramref name="handler" /> is <see langword="null" />.</exception>
public AsyncExternalEvent(Action<UIApplication, CancellationToken> handler, ExternalEventOptions options)
public AsyncExternalEvent(Action<UIApplication, CancellationToken> handler, ExternalEventOptions options) : base(handler)
{
ArgumentNullException.ThrowIfNull(handler);

Expand Down
Loading