diff --git a/lang/csharp/common.props b/lang/csharp/common.props
index e12601f3d18..f41617eadfa 100644
--- a/lang/csharp/common.props
+++ b/lang/csharp/common.props
@@ -48,7 +48,7 @@
Copyright © 2019 The Apache Software Foundation.
logo.png
- LICENSE
+ Apache-2.0
README.md
https://avro.apache.org/
Avro;Apache;Serialization;Binary;Json;Schema
diff --git a/lang/csharp/src/apache/test/NuGetPackageTests.cs b/lang/csharp/src/apache/test/NuGetPackageTests.cs
new file mode 100644
index 00000000000..493cede9312
--- /dev/null
+++ b/lang/csharp/src/apache/test/NuGetPackageTests.cs
@@ -0,0 +1,156 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using System;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Xml.Linq;
+using NUnit.Framework;
+
+namespace Avro.Test.Utils
+{
+ [TestFixture]
+ public class NuGetPackageTests
+ {
+ private static readonly string[] PackageIds = new[]
+ {
+ "Apache.Avro",
+ "Apache.Avro.Tools",
+ "Apache.Avro.File.Snappy",
+ "Apache.Avro.File.BZip2",
+ "Apache.Avro.File.XZ",
+ "Apache.Avro.File.Zstandard"
+ };
+
+ [TestCaseSource(nameof(PackageIds))]
+ public void PackageContainsSpdxLicenseExpression(string packageId)
+ {
+ var nupkgPath = FindPackageInBuildOutput(packageId);
+ if (nupkgPath == null)
+ {
+ Assert.Inconclusive($"Package {packageId} not found. Run 'dotnet pack --configuration Release' first.");
+ return;
+ }
+
+ var nuspecXml = ExtractNuspecFromPackage(nupkgPath);
+
+ // Get the namespace from the root element
+ var ns = nuspecXml.Root?.Name.Namespace ?? XNamespace.None;
+ var licenseElement = nuspecXml.Root?.Element(ns + "metadata")?.Element(ns + "license");
+
+ Assert.That(licenseElement, Is.Not.Null,
+ $"Package {packageId} does not contain a license element");
+
+ var licenseType = licenseElement?.Attribute("type")?.Value;
+ Assert.That(licenseType, Is.EqualTo("expression"),
+ $"Package {packageId} license type should be 'expression', but was '{licenseType}'");
+
+ var licenseValue = licenseElement?.Value;
+ Assert.That(licenseValue, Is.EqualTo("Apache-2.0"),
+ $"Package {packageId} should have SPDX license expression 'Apache-2.0', but was '{licenseValue}'");
+ }
+
+ [TestCaseSource(nameof(PackageIds))]
+ public void PackageContainsLicenseFile(string packageId)
+ {
+ var nupkgPath = FindPackageInBuildOutput(packageId);
+ if (nupkgPath == null)
+ {
+ Assert.Inconclusive($"Package {packageId} not found. Run 'dotnet pack --configuration Release' first.");
+ return;
+ }
+
+ using (var archive = ZipFile.OpenRead(nupkgPath))
+ {
+ var licenseEntry = archive.Entries.FirstOrDefault(e =>
+ e.FullName.Equals("LICENSE", StringComparison.OrdinalIgnoreCase));
+
+ Assert.That(licenseEntry, Is.Not.Null,
+ $"Package {packageId} does not contain LICENSE file");
+
+ Assert.That(licenseEntry.Length, Is.GreaterThan(0),
+ $"Package {packageId} LICENSE file is empty");
+ }
+ }
+
+ [TestCaseSource(nameof(PackageIds))]
+ public void PackageLicenseFileContainsApacheLicense(string packageId)
+ {
+ var nupkgPath = FindPackageInBuildOutput(packageId);
+ if (nupkgPath == null)
+ {
+ Assert.Inconclusive($"Package {packageId} not found. Run 'dotnet pack --configuration Release' first.");
+ return;
+ }
+
+ using (var archive = ZipFile.OpenRead(nupkgPath))
+ {
+ var licenseEntry = archive.Entries.FirstOrDefault(e =>
+ e.FullName.Equals("LICENSE", StringComparison.OrdinalIgnoreCase));
+
+ Assert.That(licenseEntry, Is.Not.Null);
+
+ using (var stream = licenseEntry.Open())
+ using (var reader = new StreamReader(stream))
+ {
+ var content = reader.ReadToEnd();
+ Assert.That(content, Does.Contain("Apache License"),
+ $"Package {packageId} LICENSE file does not contain Apache License text");
+ Assert.That(content, Does.Contain("Version 2.0"),
+ $"Package {packageId} LICENSE file does not specify Version 2.0");
+ }
+ }
+ }
+
+ private string FindPackageInBuildOutput(string packageId)
+ {
+ // Find the lang/csharp root (4 levels up from test binary directory: bin/Release/net8.0 -> test -> apache -> src -> csharp)
+ var testDir = TestContext.CurrentContext.TestDirectory;
+ var csharpRoot = Path.GetFullPath(Path.Combine(testDir, "..", "..", "..", "..", ".."));
+
+ // Search for the package in Release output directories
+ var pattern = $"{packageId}.*.nupkg";
+ var files = Directory.GetFiles(csharpRoot, pattern, SearchOption.AllDirectories)
+ .Where(f => f.Contains($"{Path.DirectorySeparatorChar}Release{Path.DirectorySeparatorChar}"))
+ .OrderByDescending(f => System.IO.File.GetLastWriteTime(f))
+ .ToArray();
+
+ return files.Length > 0 ? files[0] : null;
+ }
+
+ private XDocument ExtractNuspecFromPackage(string nupkgPath)
+ {
+ using (var archive = ZipFile.OpenRead(nupkgPath))
+ {
+ var nuspecEntry = archive.Entries.FirstOrDefault(e =>
+ e.FullName.EndsWith(".nuspec", StringComparison.OrdinalIgnoreCase));
+
+ if (nuspecEntry == null)
+ {
+ Assert.Fail($"No .nuspec file found in package: {nupkgPath}");
+ }
+
+ using (var stream = nuspecEntry.Open())
+ {
+ return XDocument.Load(stream);
+ }
+ }
+ }
+ }
+}