C
C#2w ago
seky16

records, with expression, required property

I'm hitting a strange behaviour and not sure if it's intended. I have the following record definition:
public record Options
{
public required IDataSource DataSource { get; init; }

public int? Take { get; init; } = null;
}
public record Options
{
public required IDataSource DataSource { get; init; }

public int? Take { get; init; } = null;
}
Then I use it in code like this:
var config = new Options() { DataSource = new DataSource(), }
assert(config.DataSource is null) // false
var config2 = config with { Take = null };
assert(config2.DataSource is null); // true
var config3 = config with { Take = null, DataSource = config.DataSource };
assert(config3.DataSource is null); // false
var config = new Options() { DataSource = new DataSource(), }
assert(config.DataSource is null) // false
var config2 = config with { Take = null };
assert(config2.DataSource is null); // true
var config3 = config with { Take = null, DataSource = config.DataSource };
assert(config3.DataSource is null); // false
Why is config2.DataSource null? Is this intended behaviour of combining required and with? If so, shouldn't I get a warning/error that I have an uninitialised required property in config2?
12 Replies
canton7
canton72w ago
Can't repro?
MODiX
MODiX2w ago
canton7
REPL Result: Success
var config = new Options() { DataSource = "DS" };
Console.WriteLine(config.DataSource is null); // false
var config2 = config with { Take = null };
Console.WriteLine(config2.DataSource is null); // true
var config3 = config with { Take = null, DataSource = config.DataSource };
Console.WriteLine(config3.DataSource is null); // false

public record Options
{
public required string DataSource { get; init; }

public int? Take { get; init; } = null;
}
var config = new Options() { DataSource = "DS" };
Console.WriteLine(config.DataSource is null); // false
var config2 = config with { Take = null };
Console.WriteLine(config2.DataSource is null); // true
var config3 = config with { Take = null, DataSource = config.DataSource };
Console.WriteLine(config3.DataSource is null); // false

public record Options
{
public required string DataSource { get; init; }

public int? Take { get; init; } = null;
}
Console Output
False
False
False
False
False
False
Compile: 514.274ms | Execution: 108.868ms | React with ❌ to remove this embed.
seky16
seky16OP2w ago
here's my repro case, it's a bit more complicated
333fred
333fred2w ago
Can you please just share code directly, rather than asking us to download zip files? You can use $paste if it's longer
MODiX
MODiX2w ago
If your code is too long, you can post to https://paste.mod.gg/, save, and copy the link into chat for others to see your shared code!
333fred
333fred2w ago
Because I'm not downloading a zip file from a random discord user, sorry
seky16
seky16OP2w ago
https://github.com/seky16/record-tc/blob/master/WebAPI/Handler.cs Not sure if it's inheritance or the IsExternalInit hack that's causing the trouble
MODiX
MODiX2w ago
333fred
REPL Result: Success
InheritedOptions config = new InheritedOptions()
{
DataSource = new EmptySource()
};

if (config.DataSource is null)
throw new InvalidProgramException(); // doesnt happen

var config2 = config with { Take = null, };

if (config2.DataSource is null)
throw new StackOverflowException(); // shouldnt happen

Console.WriteLine("Wasn't null");

public interface IDataSource;
public interface ISet;

public class EmptySource : IDataSource;

public record Options : IDataSource
{
public required IDataSource DataSource { get; init; }

public int? Take { get; init; } = null;
}

internal record InheritedOptions : Options
{
public ISet? Set { get; init; }
}
InheritedOptions config = new InheritedOptions()
{
DataSource = new EmptySource()
};

if (config.DataSource is null)
throw new InvalidProgramException(); // doesnt happen

var config2 = config with { Take = null, };

if (config2.DataSource is null)
throw new StackOverflowException(); // shouldnt happen

Console.WriteLine("Wasn't null");

public interface IDataSource;
public interface ISet;

public class EmptySource : IDataSource;

public record Options : IDataSource
{
public required IDataSource DataSource { get; init; }

public int? Take { get; init; } = null;
}

internal record InheritedOptions : Options
{
public ISet? Set { get; init; }
}
Console Output
Wasn't null
Wasn't null
Compile: 585.355ms | Execution: 99.468ms | React with ❌ to remove this embed.
333fred
333fred2w ago
Alright, so this is something with your multi-targeting If I switch your Expressions project to net8.0, it works fine
333fred
333fred2w ago
@seky16 looks like we have an existing bug on this, https://github.com/dotnet/roslyn/issues/72357. Can you add a comment to ping the issue again?
GitHub
Issues · dotnet/roslyn
The Roslyn .NET compiler provides C# and Visual Basic languages with rich code analysis APIs. - Issues · dotnet/roslyn
333fred
333fred2w ago
Good news @seky16, you nerd sniped me into fixing this: https://github.com/dotnet/roslyn/pull/76347
GitHub
Do not bail generating base type initializer in the presence of use...
We were returning early from generating a call to the base record copy constructor initializer if use site diagnostics reported any kind of diagnostic, including warnings. This isn't good i...
seky16
seky16OP2w ago
thanks! yeah workaround with targeting net8.0 works, so that's great
Want results from more Discord servers?
Add your server