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
10 changes: 10 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
27 changes: 24 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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/
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -31,7 +31,7 @@ jobs:
destination: ./_site

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
uses: actions/upload-pages-artifact@v5

deploy:
environment:
Expand All @@ -42,4 +42,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@v5
12 changes: 11 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

<!-- NuGet Package Metadata -->
<PropertyGroup>
<!-- 1.0.1: fixes 1.0.0 packaging bug where the source generator was never delivered to consumers. -->
<Version>1.0.1</Version>
<Authors>D. Ivahno</Authors>
<Company>OpenAutoMapper</Company>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
14 changes: 7 additions & 7 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,19 @@
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />

<!-- DI -->
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.5" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.8" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.5" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.8" />

<!-- SourceLink -->
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.201" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.300" />

<!-- Testing -->
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<PackageVersion Include="coverlet.collector" Version="8.0.1" />
<PackageVersion Include="FluentAssertions" Version="8.9.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
<PackageVersion Include="coverlet.collector" Version="10.0.1" />
<PackageVersion Include="FluentAssertions" Version="8.10.0" />

<!-- Verify (snapshot testing) -->
<PackageVersion Include="Verify.Xunit" Version="31.12.5" />
Expand All @@ -42,7 +42,7 @@

<!-- Competitive benchmarks -->
<PackageVersion Include="AutoMapper" Version="16.1.1" />
<PackageVersion Include="Mapster" Version="10.0.3" />
<PackageVersion Include="Mapster" Version="10.0.7" />
<PackageVersion Include="Riok.Mapperly" Version="4.3.1" />
</ItemGroup>

Expand Down
149 changes: 149 additions & 0 deletions eng/package-smoke-test.sh
Original file line number Diff line number Diff line change
@@ -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
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="local" value="$FEED" />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>
EOF

cat > "$CONSUMER/consumer.csproj" <<EOF
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<!-- Single install must deliver the generator (1.0.0 packaging bug). -->
<PackageReference Include="OpenAutoMapper" Version="$PKG_VERSION" />
<!-- DI-only install must also deliver the generator transitively. -->
<PackageReference Include="OpenAutoMapper.DependencyInjection" Version="$PKG_VERSION" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
</ItemGroup>
</Project>
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<Order, OrderDto>(); }

public static class Program
{
public static int Main()
{
// 1) Direct MapperConfiguration flow (README quick start).
var config = new MapperConfiguration(cfg => cfg.AddProfile<OrderProfile>());
var mapper = config.CreateMapper();
var dto = mapper.Map<OrderDto>(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<Order, OrderDto>(), typeof(Program).Assembly)
.BuildServiceProvider();
var diMapper = sp.GetRequiredService<IMapper>();
var diDto = diMapper.Map<OrderDto>(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"
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,16 @@
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="../OpenAutoMapper/OpenAutoMapper.csproj" />
<!--
PrivateAssets="contentfiles" (instead of the default contentfiles;analyzers;build)
lets the source generator bundled in the OpenAutoMapper meta-package flow
transitively to consumers who install ONLY this DI package. Without it, the
default exclude="Build,Analyzers" on this dependency edge would strip the
analyzer and the generator would silently produce nothing. Only one physical
copy of the analyzer exists (in OpenAutoMapper), so installing both packages
does not double-load the generator.
-->
<ProjectReference Include="../OpenAutoMapper/OpenAutoMapper.csproj" PrivateAssets="contentfiles" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
</ItemGroup>
</Project>
17 changes: 15 additions & 2 deletions src/OpenAutoMapper.Generator/Pipeline/MapperImplEmitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,17 +117,30 @@ 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<global::OpenAutoMapper.MapperConfiguration, global::OpenAutoMapper.IMapper>(");
sb.Indent();
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.MapperConfiguration, global::System.Func<global::System.Type, object>, global::OpenAutoMapper.IMapper>(");
sb.Indent();
sb.AppendLine("(config, serviceCtor) => new OpenAutoMapperImpl()));");
sb.Unindent();
sb.CloseBrace();

sb.CloseBrace();

Expand Down
Loading
Loading