diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 317f378..92d89df 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -7,6 +7,16 @@ updates:
open-pull-requests-limit: 10
labels:
- "dependencies"
+ ignore:
+ # The source generator references Roslyn and runs inside the consumer's
+ # compiler. Building against a newer major (5.x ships with the .NET 10 SDK)
+ # makes the generator emit CS9057 and silently NOT run for consumers on the
+ # .NET 8/9 SDKs. Stay on the 4.x/3.x line "for SDK compatibility"; allow
+ # only minor/patch bumps.
+ - dependency-name: "Microsoft.CodeAnalysis.CSharp"
+ update-types: ["version-update:semver-major"]
+ - dependency-name: "Microsoft.CodeAnalysis.Analyzers"
+ update-types: ["version-update:semver-major"]
groups:
microsoft:
patterns:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a59f697..a699cd0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -49,14 +49,14 @@ jobs:
- name: Upload Test Results
if: always()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
name: test-results-${{ matrix.os }}-net${{ matrix.dotnet }}
path: TestResults/
- name: Upload Coverage
if: matrix.os == 'ubuntu-latest' && matrix.dotnet == '10.0.x'
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
name: coverage-report
path: TestResults/**/coverage.cobertura.xml
@@ -92,6 +92,27 @@ jobs:
exit 1
fi
+ package-smoke:
+ name: Packaged Consumer Smoke Test
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: |
+ 8.0.x
+ 9.0.x
+ 10.0.x
+
+ # Packs the real packages, installs them into a throwaway consumer, and
+ # asserts the source generator is delivered and runs (direct + DI flows).
+ # This is the only check that exercises packaging the way a consumer does;
+ # the unit/integration projects do not wire the generator as an analyzer.
+ - name: Pack + install + run a real consumer
+ run: ./eng/package-smoke-test.sh
+
format-check:
name: Format Check
runs-on: ubuntu-latest
@@ -176,7 +197,7 @@ jobs:
--artifacts benchmark-results
- name: Upload Benchmark Results
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
name: benchmark-results
path: benchmark-results/
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index b874914..a653a9c 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -22,7 +22,7 @@ jobs:
- uses: actions/checkout@v4
- name: Setup Pages
- uses: actions/configure-pages@v5
+ uses: actions/configure-pages@v6
- name: Build with Jekyll
uses: actions/jekyll-build-pages@v1
@@ -31,7 +31,7 @@ jobs:
destination: ./_site
- name: Upload artifact
- uses: actions/upload-pages-artifact@v3
+ uses: actions/upload-pages-artifact@v5
deploy:
environment:
@@ -42,4 +42,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
- uses: actions/deploy-pages@v4
+ uses: actions/deploy-pages@v5
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 4e84dd5..329fdcb 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -52,6 +52,16 @@ jobs:
echo "Packages created:"
ls -la nupkg/
+ - name: Verify generator is bundled in the meta-package
+ run: |
+ PKG="nupkg/OpenAutoMapper.${{ env.VERSION }}.nupkg"
+ if ! unzip -l "$PKG" | grep -q "analyzers/dotnet/cs/OpenAutoMapper.Generator.dll"; then
+ echo "::error::$PKG does not contain the source generator (analyzers/dotnet/cs/OpenAutoMapper.Generator.dll). Refusing to publish a package that would generate nothing for consumers."
+ unzip -l "$PKG"
+ exit 1
+ fi
+ echo "OK: source generator is bundled in $PKG"
+
- name: Generate SBOM
run: |
dotnet tool install -g Microsoft.Sbom.DotNetTool || true
@@ -75,7 +85,7 @@ jobs:
--skip-duplicate
- name: Create GitHub Release
- uses: softprops/action-gh-release@v2
+ uses: softprops/action-gh-release@v3
with:
files: |
nupkg/*.*nupkg
diff --git a/Directory.Build.props b/Directory.Build.props
index 404f179..4340b1f 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -11,6 +11,8 @@
+
+ 1.0.1
D. Ivahno
OpenAutoMapper
MIT
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 54c2973..5826956 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -11,19 +11,19 @@
-
+
-
+
-
+
-
-
-
+
+
+
@@ -42,7 +42,7 @@
-
+
diff --git a/eng/package-smoke-test.sh b/eng/package-smoke-test.sh
new file mode 100755
index 0000000..fc88ab4
--- /dev/null
+++ b/eng/package-smoke-test.sh
@@ -0,0 +1,149 @@
+#!/usr/bin/env bash
+#
+# Packaged-consumer smoke test.
+#
+# Packs the real NuGet packages, installs them into a throwaway consumer project,
+# and asserts that the source generator is actually DELIVERED and RUNS — i.e. that
+# `dotnet add package OpenAutoMapper` produces working mapping code, and that
+# resolving IMapper through the DI package works.
+#
+# This is the test that would have caught the 1.0.0 packaging bug (generator never
+# shipped to consumers) and the DI factory bug (MapperFactoryWithServiceCtor never
+# registered, so IMapper resolution threw). The in-repo test projects do NOT wire
+# the generator as an analyzer the way a real consumer does, so they cannot catch
+# these regressions.
+#
+# Usage: eng/package-smoke-test.sh
+# Requires: dotnet SDK on PATH. Network access to nuget.org (for the BCL packages).
+
+set -euo pipefail
+
+REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+WORK="$(mktemp -d)"
+FEED="$WORK/feed"
+CONSUMER="$WORK/consumer"
+CONFIG="${CONFIGURATION:-Release}"
+trap 'rm -rf "$WORK"' EXIT
+
+echo "==> Building solution, then packing to local feed: $FEED"
+mkdir -p "$FEED"
+# Build once, then pack with --no-build (exactly what release.yml does). This is
+# deterministic: with --no-build there are no parallel per-TFM inner builds during
+# pack, so pack's collection of the static None analyzer item cannot race the
+# generator's build. (Plain `dotnet pack` rebuilds during pack and intermittently
+# drops the analyzer when the multi-TFM inner builds race the file collection.)
+if ! dotnet build "$REPO_ROOT/OpenAutoMapper.slnx" -c "$CONFIG" >/dev/null; then
+ echo "FAIL: solution build failed"
+ exit 1
+fi
+for proj in \
+ OpenAutoMapper.Abstractions \
+ OpenAutoMapper.Core \
+ OpenAutoMapper \
+ OpenAutoMapper.DependencyInjection; do
+ if ! dotnet pack "$REPO_ROOT/src/$proj/$proj.csproj" -c "$CONFIG" --no-build -o "$FEED" >/dev/null; then
+ echo "FAIL: dotnet pack --no-build failed for $proj"
+ exit 1
+ fi
+done
+
+PKG_VERSION="$(ls "$FEED"/OpenAutoMapper.[0-9]*.nupkg 2>/dev/null | head -1 | sed -E 's/.*OpenAutoMapper\.([0-9][^/]*)\.nupkg/\1/')"
+META_PKG="$FEED/OpenAutoMapper.$PKG_VERSION.nupkg"
+if [ -z "$PKG_VERSION" ] || [ ! -f "$META_PKG" ]; then
+ echo "FAIL: OpenAutoMapper package was not produced in $FEED"
+ ls -la "$FEED" || true
+ exit 1
+fi
+echo "==> Packed version: $PKG_VERSION"
+
+# Assert the analyzer DLL is physically present in the meta-package.
+echo "==> Verifying analyzer is bundled in the OpenAutoMapper package"
+if ! unzip -l "$META_PKG" | grep -q "analyzers/dotnet/cs/OpenAutoMapper.Generator.dll"; then
+ echo "FAIL: $META_PKG exists but does not contain analyzers/dotnet/cs/OpenAutoMapper.Generator.dll"
+ exit 1
+fi
+
+echo "==> Creating consumer project: $CONSUMER"
+mkdir -p "$CONSUMER"
+cat > "$CONSUMER/nuget.config" <
+
+
+
+
+
+
+
+EOF
+
+cat > "$CONSUMER/consumer.csproj" <
+
+ Exe
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+EOF
+
+cat > "$CONSUMER/Program.cs" <<'EOF'
+using OpenAutoMapper;
+using Microsoft.Extensions.DependencyInjection;
+
+public class Order { public int Id { get; set; } public string CustomerName { get; set; } = ""; public decimal Total { get; set; } }
+public class OrderDto { public int Id { get; set; } public string CustomerName { get; set; } = ""; public decimal Total { get; set; } }
+public class OrderProfile : Profile { public OrderProfile() => CreateMap(); }
+
+public static class Program
+{
+ public static int Main()
+ {
+ // 1) Direct MapperConfiguration flow (README quick start).
+ var config = new MapperConfiguration(cfg => cfg.AddProfile());
+ var mapper = config.CreateMapper();
+ var dto = mapper.Map(new Order { Id = 1, CustomerName = "Jane Doe", Total = 49.99m });
+ if (dto.Id != 1 || dto.CustomerName != "Jane Doe" || dto.Total != 49.99m)
+ {
+ System.Console.Error.WriteLine("FAIL: direct mapping produced wrong result");
+ return 1;
+ }
+
+ // 2) DI flow — resolving IMapper exercises CreateMapper(serviceCtor).
+ var sp = new ServiceCollection()
+ .AddAutoMapper(cfg => cfg.CreateMap(), typeof(Program).Assembly)
+ .BuildServiceProvider();
+ var diMapper = sp.GetRequiredService();
+ var diDto = diMapper.Map(new Order { Id = 2, CustomerName = "DI", Total = 10m });
+ if (diDto.Id != 2 || diDto.CustomerName != "DI")
+ {
+ System.Console.Error.WriteLine("FAIL: DI mapping produced wrong result");
+ return 1;
+ }
+
+ System.Console.WriteLine("SMOKE OK: direct + DI mapping work from packaged generator");
+ return 0;
+ }
+}
+EOF
+
+echo "==> Building and running consumer (generator must run as a real analyzer)"
+cd "$CONSUMER"
+if ! dotnet run -c "$CONFIG" 2>&1 | tee "$WORK/run.log"; then
+ echo "FAIL: consumer run failed — generator likely not delivered"
+ exit 1
+fi
+
+if ! grep -q "SMOKE OK" "$WORK/run.log"; then
+ echo "FAIL: consumer did not print success marker"
+ exit 1
+fi
+
+echo "==> PASS: packaged-consumer smoke test succeeded"
diff --git a/src/OpenAutoMapper.DependencyInjection/OpenAutoMapper.DependencyInjection.csproj b/src/OpenAutoMapper.DependencyInjection/OpenAutoMapper.DependencyInjection.csproj
index 8499235..aee5c9d 100644
--- a/src/OpenAutoMapper.DependencyInjection/OpenAutoMapper.DependencyInjection.csproj
+++ b/src/OpenAutoMapper.DependencyInjection/OpenAutoMapper.DependencyInjection.csproj
@@ -7,7 +7,16 @@
-
+
+
diff --git a/src/OpenAutoMapper.Generator/Pipeline/MapperImplEmitter.cs b/src/OpenAutoMapper.Generator/Pipeline/MapperImplEmitter.cs
index eab2149..d55004e 100644
--- a/src/OpenAutoMapper.Generator/Pipeline/MapperImplEmitter.cs
+++ b/src/OpenAutoMapper.Generator/Pipeline/MapperImplEmitter.cs
@@ -117,10 +117,13 @@ public static string EmitFactoryInitializer()
sb.AppendLine("// Register the generated mapper factory so that MapperConfiguration.CreateMapper() works.");
sb.AppendLine("// Uses reflection to set the internal static property since the generator cannot reference Core directly.");
sb.AppendLine("var configType = typeof(global::OpenAutoMapper.MapperConfiguration);");
- sb.AppendLine("var factoryProperty = configType.GetProperty(\"MapperFactory\",");
+ sb.AppendLine("const System.Reflection.BindingFlags flags =");
sb.Indent();
- sb.AppendLine("System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);");
+ sb.AppendLine("System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic;");
sb.Unindent();
+ sb.AppendLine();
+ sb.AppendLine("// Parameterless factory: MapperConfiguration.CreateMapper().");
+ sb.AppendLine("var factoryProperty = configType.GetProperty(\"MapperFactory\", flags);");
sb.AppendLine("if (factoryProperty is not null)");
sb.OpenBrace();
sb.AppendLine("factoryProperty.SetValue(null, new global::System.Func(");
@@ -128,6 +131,16 @@ public static string EmitFactoryInitializer()
sb.AppendLine("config => new OpenAutoMapperImpl()));");
sb.Unindent();
sb.CloseBrace();
+ sb.AppendLine();
+ sb.AppendLine("// Service-constructor factory: MapperConfiguration.CreateMapper(serviceCtor), used by AddAutoMapper() DI.");
+ sb.AppendLine("var factoryWithCtorProperty = configType.GetProperty(\"MapperFactoryWithServiceCtor\", flags);");
+ sb.AppendLine("if (factoryWithCtorProperty is not null)");
+ sb.OpenBrace();
+ sb.AppendLine("factoryWithCtorProperty.SetValue(null, new global::System.Func, global::OpenAutoMapper.IMapper>(");
+ sb.Indent();
+ sb.AppendLine("(config, serviceCtor) => new OpenAutoMapperImpl()));");
+ sb.Unindent();
+ sb.CloseBrace();
sb.CloseBrace();
diff --git a/src/OpenAutoMapper/OpenAutoMapper.csproj b/src/OpenAutoMapper/OpenAutoMapper.csproj
index ccaa61e..8464ce5 100644
--- a/src/OpenAutoMapper/OpenAutoMapper.csproj
+++ b/src/OpenAutoMapper/OpenAutoMapper.csproj
@@ -14,5 +14,47 @@
ReferenceOutputAssembly="false" />
+
+
+ <_GeneratorAssemblyPath>$(MSBuildThisFileDirectory)../OpenAutoMapper.Generator/bin/$(Configuration)/netstandard2.0/OpenAutoMapper.Generator.dll
+ <_StagedGeneratorDir>$(BaseIntermediateOutputPath)gen-analyzer
+ <_StagedGeneratorPath>$(_StagedGeneratorDir)/OpenAutoMapper.Generator.dll
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/OpenAutoMapper.Generator.Tests/GeneratorSnapshotTests.MapperImpl.cs b/tests/OpenAutoMapper.Generator.Tests/GeneratorSnapshotTests.MapperImpl.cs
index ab17713..62ac196 100644
--- a/tests/OpenAutoMapper.Generator.Tests/GeneratorSnapshotTests.MapperImpl.cs
+++ b/tests/OpenAutoMapper.Generator.Tests/GeneratorSnapshotTests.MapperImpl.cs
@@ -96,6 +96,26 @@ public class TestProfile : Profile { public TestProfile() { CreateMap s.Contains("OpenAutoMapperFactoryInit"));
}
+ [Fact]
+ public void MapperImpl_FactoryInitializer_RegistersBothFactories()
+ {
+ // Regression: the factory initializer must register BOTH MapperFactory
+ // (CreateMapper()) and MapperFactoryWithServiceCtor (CreateMapper(serviceCtor)).
+ // The DI package resolves IMapper via CreateMapper(serviceCtor); when only the
+ // parameterless factory was registered, AddAutoMapper() worked for config but
+ // threw "No mapper factory registered" the moment IMapper was actually resolved.
+ var source = @"
+using OpenAutoMapper;
+namespace TestApp;
+public class Source { public int Id { get; set; } }
+public class Dest { public int Id { get; set; } }
+public class TestProfile : Profile { public TestProfile() { CreateMap(); } }
+";
+ var (_, generatedSources) = TestHelper.RunGenerator(source);
+ generatedSources.Should().Contain(s => s.Contains("MapperFactory"));
+ generatedSources.Should().Contain(s => s.Contains("MapperFactoryWithServiceCtor"));
+ }
+
[Fact]
public void MapperImpl_NonGenericMapOverloads()
{