Skip to content
Open
Show file tree
Hide file tree
Changes from 10 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
12 changes: 6 additions & 6 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-dotnet@v3
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
- name: Restore dependencies
run: dotnet restore /p:TreatWarningsAsErrors=true
- name: Build
Expand All @@ -22,12 +22,12 @@ jobs:
name: Generate NuGet Packages
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-dotnet@v3
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
name: Install Current .NET SDK
- name: Generate NuGet Packages
run: dotnet pack --configuration Release --output nupkg /p:TreatWarningsAsErrors=true
- uses: actions/upload-artifact@v2
run: dotnet pack --output nupkg /p:TreatWarningsAsErrors=true
- uses: actions/upload-artifact@v4
if: success() && github.ref == 'refs/heads/main'
with:
name: nupkg
Expand Down
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
bin/
obj/
/artifacts/
8 changes: 4 additions & 4 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@
<Copyright>© Polyadic. All rights reserved.</Copyright>
</PropertyGroup>
<ItemGroup Label="Code Style">
<PackageReference Include="Messerli.CodeStyle" PrivateAssets="all" />
<PackageReference Include="Polyadic.CodeStyle" PrivateAssets="all" />
</ItemGroup>
<PropertyGroup Label="Deterministic Builds and Source Link">
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<ContinuousIntegrationBuild Condition="'$(GITHUB_ACTIONS)' == 'true'">true</ContinuousIntegrationBuild>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
</PropertyGroup>
<ItemGroup Label="Deterministic Builds and Source Link">
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All"/>
</ItemGroup>
<PropertyGroup>
<ArtifactsPath>$(MSBuildThisFileDirectory)artifacts</ArtifactsPath>
</PropertyGroup>
</Project>
9 changes: 4 additions & 5 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup Label="Build Dependencies">
<PackageVersion Include="Messerli.CodeStyle" Version="2.2.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" />
<PackageVersion Include="Polyadic.CodeStyle" Version="1.1.0" />
</ItemGroup>
<ItemGroup Label="Test Dependencies">
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageVersion Include="xunit" Version="2.4.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.4.5" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageVersion Include="xunit.v3" Version="1.0.1" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.0.1" />
</ItemGroup>
</Project>
83 changes: 83 additions & 0 deletions Dispownership.Test/AsyncDisposableTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
namespace Dispownership.Test;

public sealed class AsyncDisposableTest
{
[Fact]
public async Task DisposesInnerValueIfOwned()
{
var value = new AsyncDisposableStub();

await using (AsyncDisposable.Owned(value))
{
}

Assert.True(value.Disposed);
}

[Fact]
public async Task DoesNotDisposeInnerValueIfBorrowed()
{
var value = new AsyncDisposableStub();

await using (AsyncDisposable.Borrowed(value))
{
}

Assert.False(value.Disposed);
}

[Fact]
public async Task DoesNotDisposeOwnedValueAfterMove()
{
var value = new AsyncDisposableStub();

await using (var disposable = AsyncDisposable.Owned(value))
{
_ = disposable.Take();
}

Assert.False(value.Disposed);
}

[Fact]
public async Task ThrowsWhenMovingAValueTwice()
{
await using var disposable = AsyncDisposable.Owned(new AsyncDisposableStub());
_ = disposable.Take();
Assert.Throws<InvalidOperationException>(() => disposable.Take());
}

[Fact]
public async Task ThrowsWhenMovingABorrowedValue()
{
await using var disposable = AsyncDisposable.Borrowed(new AsyncDisposableStub());
Assert.Throws<InvalidOperationException>(() => disposable.Take());
}

[Theory]
[MemberData(nameof(Disposables))]
public async Task ThrowsWhenDisposingTwice(AsyncDisposable<AsyncDisposableStub> disposable)
{
#pragma warning disable IDISP007
await disposable.DisposeAsync();
#pragma warning restore IDISP007
await Assert.ThrowsAsync<ObjectDisposedException>(async () => await disposable.DisposeAsync());
}

public static TheoryData<AsyncDisposable<AsyncDisposableStub>> Disposables()
=> [
AsyncDisposable.Owned(new AsyncDisposableStub()),
AsyncDisposable.Borrowed(new AsyncDisposableStub()),
];

public sealed class AsyncDisposableStub : IAsyncDisposable
{
public bool Disposed { get; private set; }

public ValueTask DisposeAsync()
{
Disposed = true;
return ValueTask.CompletedTask;
}
}
}
23 changes: 19 additions & 4 deletions Dispownership.Test/DisposableTest.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
using System;
using Xunit;

namespace Dispownership.Test;

public sealed class DisposableTest
Expand Down Expand Up @@ -67,7 +64,25 @@ public void ThrowsWhenMovingABorrowedValue()
Assert.Throws<InvalidOperationException>(() => disposable.Take());
}

private sealed class DisposableStub : IDisposable
[Theory]
[MemberData(nameof(Disposables))]
public void ThrowsWhenDisposingTwice(Disposable<DisposableStub> disposable)
{
#pragma warning disable IDISP007
disposable.Dispose();
#pragma warning restore IDISP007
Assert.Throws<ObjectDisposedException>(disposable.Dispose);
}

#pragma warning disable IDISP004
public static TheoryData<Disposable<DisposableStub>> Disposables()
=> [
Disposable.Owned(new DisposableStub()),
Disposable.Borrowed(new DisposableStub()),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see why this test case should throw on double dispose, isn't this the point, that it is not disposed when borrowed? Maybe I just don't understand.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the inner disposable is not disposed when borrowed. What I changed is that the wrapper tracks that it got disposed and throws in that case. I thought it would be a good thing for consistency since that's what other disposables do too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, never mind I misunderstood the dispose pattern 😆

Dispose should do nothing when called multiple times, but anything that accesses the resource should throw.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we should implement this for our consumers. I know this is correct according the dispose pattern as per Microsoft, but if we track dispose, the contained value can't decide if it throws when used after dispose.

This is a huge change and not necessarily in the interest of consumers, I fear? What do you think? There might be existing code that doesn't care if dispose is called, and then take after. This would be a breaking change for them, at least theoretically.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree it might be a bit of a change, but I think it's a good one :)

Consumers of the disposable wrapper currently see two different behaviours when the wrapper is disposed:

  • if owned, they might get an ObjectDisposedException when trying to do anything on the inner object
  • if borrowed, they don't

One of the use cases of the disposable is to treat owned and borrowed objects the same way. Always throwing if disposed might help catch bugs :)

];
#pragma warning restore IDISP004

public sealed class DisposableStub : IDisposable
{
public bool Disposed { get; private set; }

Expand Down
10 changes: 8 additions & 2 deletions Dispownership.Test/Dispownership.Test.csproj
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<IsPackable>false</IsPackable>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Dispownership\Dispownership.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.v3" />
<PackageReference Include="xunit.runner.visualstudio" />
</ItemGroup>
</Project>
23 changes: 19 additions & 4 deletions Dispownership/AsyncDisposable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#if (NETSTANDARD2_1 || NET5_0_OR_GREATER) && DISPOWNERSHIP_ASYNC
using System;
using System.ComponentModel;
using System.Threading.Tasks;

namespace Dispownership;
Expand Down Expand Up @@ -43,7 +44,9 @@ sealed class AsyncDisposable<TDisposable> : IAsyncDisposable
{
private readonly TDisposable _inner;
private bool _hasOwnership;
private bool _disposed;

[EditorBrowsable(EditorBrowsableState.Never)]
internal AsyncDisposable(TDisposable inner, bool hasOwnership)
{
_inner = inner;
Expand All @@ -68,11 +71,23 @@ public TDisposable Take()
return _inner;
}

#pragma warning disable IDISP007
public ValueTask DisposeAsync()
=> _hasOwnership
? (_inner?.DisposeAsync() ?? default)
: default;
{
if (_disposed)
{
return ValueTask.FromException(new ObjectDisposedException(nameof(AsyncDisposable)));
}

_disposed = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does exactly what the test shows - but I'm not sure it is correct, sorry!


if (_hasOwnership)
{
#pragma warning disable IDISP007
return _inner.DisposeAsync();
#pragma warning restore IDISP007
}

return default;
}
}
#endif
12 changes: 11 additions & 1 deletion Dispownership/Disposable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#pragma warning disable IDE0005 // Disable unused usings warning if enabled in consuming project

using System;
using System.ComponentModel;

namespace Dispownership;

Expand Down Expand Up @@ -41,7 +42,9 @@ sealed class Disposable<TDisposable> : IDisposable
{
private readonly TDisposable _inner;
private bool _hasOwnership;
private bool _disposed;

[EditorBrowsable(EditorBrowsableState.Never)]
internal Disposable(TDisposable inner, bool hasOwnership)
{
_inner = inner;
Expand All @@ -68,10 +71,17 @@ public TDisposable Take()

public void Dispose()
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(Disposable));
}

_disposed = true;

if (_hasOwnership)
{
#pragma warning disable IDISP007
_inner?.Dispose();
_inner.Dispose();
#pragma warning restore IDISP007
}
}
Expand Down
6 changes: 6 additions & 0 deletions Dispownership/Dispownership.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,15 @@
<PackageReadmeFile>readme.md</PackageReadmeFile>
</PropertyGroup>
<PropertyGroup>
<EnablePackageValidation>true</EnablePackageValidation>
</PropertyGroup>
<PropertyGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net6.0'))">
<IsTrimmable>true</IsTrimmable>
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
</PropertyGroup>
<PropertyGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
<ItemGroup>
<None Include="..\readme.md" Pack="true" PackagePath="\" />
</ItemGroup>
Expand Down
8 changes: 4 additions & 4 deletions global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "7.0.201",
"rollForward": "feature"
}
"sdk": {
"version": "9.0.100",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might have to bump this to .102 for the pipeline to work, 101 and 100 are not usable through actions/ci anymore (marked as insecure by msft)

"rollForward": "feature"
}
}