Skip to content

Enable saving on .NET Core #1307

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jan 26, 2023
Merged
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [3.0.0]

## Added
- Packages can now be saved on .NET Core and .NET 5+ if constructed with a path or stream (#1307).

## Changed
- When validation finds incorrect part, it will now include the relationship type rather than a class name

## Removed
- .NET Standard 1.3 is no longer a supported platform. .NET Standard 2.0 is the lowest .NET Standard supported.

### Breaking change
- OpenXmlPackage.CanSave is now an instance method (#1307)
- IdPartPair is now a readonly struct rather than a class
- IDisposableFeature is now a part of the framework package and is available by default on a package. Extension methods to manage this feature have been removed as it no longer needs to be opted into. It now registers all disposable actions to be done at the package level instead of adding support at the part level.
- OpenXmlPackage.CanSave is now an instance method
- Core infrastructure is now contained in a new package DocumentFormat.OpenXml.Framework. Typed classes are still in DocumentFormat.OpenXml. This means that you may reference DocumentFormat.OpenXml and still compile the same types, but if you want a smaller package, you may rely on just the framework package.
- Removed `OpenXmlPackage.Package` property. A `OpenXmlPackage` is now backed by a `IPackage` instead of `System.IO.Packaging.Package`. This can be retrieved by `OpenXmlPackage.Features.Get<IPackageFeature>()`
- Renamed PartExtensionProvider to IPartExtensionFeature and reduced its surface area to only two methods (instead of a full Dictionary<,>). The property to access this off of OpenXmlPackage has been removed, but may be accessed via `Features.Get<IPartExtensionFeature>()` if needed.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace DocumentFormat.OpenXml.Packaging.Builder;

internal static class OpenXmlPackageInitializerExtensions
{
public static void InitializePackage(this OpenXmlPackage package) => package.Features
.EnableSavePackage();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using DocumentFormat.OpenXml.Features;
using System.IO;

namespace DocumentFormat.OpenXml.Packaging.Builder;

internal static class SavePackageExtensions
{
internal static IFeatureCollection EnableSavePackage(this IFeatureCollection features)
{
var feature = features.GetRequired<IPackageFeature>();

// No need to enable saving if package is only read-only
if (feature.Package.FileOpenAccess == FileAccess.Read)
{
return features;
}

var capabilities = feature.Capabilities;

if (!capabilities.HasFlagFast(PackageCapabilities.Save) && capabilities.HasFlagFast(PackageCapabilities.Reload))
{
features.Set<IPackageFeature>(new SaveablePackage(feature));
}

return features;
}

private sealed class SaveablePackage : DelegatingPackageFeature
{
public SaveablePackage(IPackageFeature package)
: base(package)
{
}

public override PackageCapabilities Capabilities => base.Capabilities | PackageCapabilities.Save;

public override void Save() => Feature.Reload();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Features;
using DocumentFormat.OpenXml.Packaging.Builder;
using System;
using System.Collections.Generic;
using System.IO;
Expand Down Expand Up @@ -66,6 +67,8 @@ private protected OpenXmlPackage(IPackageFeature packageFeature, OpenSettings? s
Features.GetRequired<IDisposableFeature>().Register(disposable);
}

this.InitializePackage();

Load(packageFeature.Package);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using DocumentFormat.OpenXml.Features;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Packaging.Builder;
using NSubstitute;
using System.IO;
using Xunit;

namespace DocumentFormat.OpenXml.Framework.Tests.Features;

public class SaveablePackageTests
{
[InlineData((int)PackageCapabilities.Save, FileAccess.Read, false)]
[InlineData((int)PackageCapabilities.Save, FileAccess.ReadWrite, false)]
[InlineData((int)PackageCapabilities.Reload, FileAccess.Read, false)]
[InlineData((int)PackageCapabilities.Reload, FileAccess.ReadWrite, true)]
[InlineData((int)PackageCapabilities.Save | (int)PackageCapabilities.Reload, FileAccess.Read, false)]
[InlineData((int)PackageCapabilities.Save | (int)PackageCapabilities.Reload, FileAccess.ReadWrite, false)]
[Theory]
public void RequiredCapabilityCheck(int intCapabilities, FileAccess access, bool shouldUpdate)
{
// Arrange
var capabilities = (PackageCapabilities)intCapabilities;
var features = new FeatureCollection();

var package = Substitute.For<IPackage>();
package.FileOpenAccess.Returns(access);

var feature = Substitute.For<IPackageFeature>();
feature.Capabilities.Returns(capabilities);
feature.Package.Returns(package);

features.Set(feature);

// Act
var updatedFeatures = features.EnableSavePackage();

// Assert
var updatedFeature = features.GetRequired<IPackageFeature>();
Assert.Same(features, updatedFeatures);

if (shouldUpdate)
{
Assert.Equal(updatedFeature.Capabilities, capabilities | PackageCapabilities.Save);
Assert.NotSame(feature, updatedFeature);
}
else
{
Assert.Same(feature, updatedFeature);
}
}

[Fact]
public void SaveCallsReload()
{
// Arrange
var features = new FeatureCollection();

var feature = Substitute.For<IPackageFeature>();
feature.Capabilities.Returns(PackageCapabilities.Reload);
features.Set(feature);

// Act
features.EnableSavePackage();
var updatedFeature = features.GetRequired<IPackageFeature>();
updatedFeature.Package.Save();

// Assert
feature.Received(1).Reload();
Assert.Equal(PackageCapabilities.Reload | PackageCapabilities.Save, updatedFeature.Capabilities);
}
}
22 changes: 4 additions & 18 deletions test/DocumentFormat.OpenXml.Tests/SaveAndCloneTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -369,26 +369,12 @@ byte[] GetNewSpreadsheet()

var bytes = GetNewSpreadsheet();

bool canSave =
#if !NET6_0_OR_GREATER
true;
#else
false;
#endif

if (canSave)
{
Assert.NotEmpty(bytes);
Assert.NotEmpty(bytes);

using (var stream = new MemoryStream(bytes))
using (var source = SpreadsheetDocument.Open(stream, false))
{
Assert.Single(source.WorkbookPart.Workbook.ChildElements);
}
}
else
using (var stream = new MemoryStream(bytes))
using (var source = SpreadsheetDocument.Open(stream, false))
{
Assert.Empty(bytes);
Assert.Single(source.WorkbookPart.Workbook.ChildElements);
}
}

Expand Down