Skip to content
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

Add basic support for 'dotnet run file.cs' #46915

Open
wants to merge 15 commits into
base: main
Choose a base branch
from

Conversation

jjonescz
Copy link
Member

@jjonescz jjonescz commented Feb 18, 2025

@dotnet-issue-labeler dotnet-issue-labeler bot added Area-CLI untriaged Request triage from a team member labels Feb 18, 2025
@jjonescz jjonescz marked this pull request as ready for review February 19, 2025 12:15
@Copilot Copilot bot review requested due to automatic review settings February 19, 2025 12:15
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot reviewed 20 out of 20 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (2)

src/Cli/dotnet/commands/dotnet-run/RunCommand.cs:609

  • [nitpick] The method 'DiscoverProjectFilePath' now returns both a project file path and an entry point file via an out parameter; consider renaming it (for example, to 'DiscoverProjectAndEntryPointPaths') to better reflect its dual purpose.
private static string? DiscoverProjectFilePath(string? projectFileOrDirectoryPath, ref string[] args, out string? entryPointFilePath)

src/Cli/dotnet/commands/dotnet-run/RunCommand.cs:250

  • [nitpick] Consider renaming the 'projectFactory' out parameter to 'projectInstanceFactory' for improved clarity since it is intended to supply a ProjectInstance.
private void EnsureProjectIsBuilt(out Func<ProjectCollection, ProjectInstance>? projectFactory)

@@ -205,6 +199,16 @@ private static string GetDotnetPath()
return new Muxer().MuxerPath;
}

internal static Dictionary<string, string> GetMSBuildRequiredEnvironmentVariables()
Copy link
Member

Choose a reason for hiding this comment

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

This call is now being used in a few places, sometimes to create a new dictionary and sometimes to just iterate this data which is effectively static. Did you consider instead having a definition like this:

 internal static ImmutableDictionary<string, string> MSBuildRequiredEnvironmentVariables { get; } = ...

Then in the places where a new dictionary is needed use new (MSBuildRequiredEnvironmentVariables) and for iteration it can just use the property directly.

Copy link
Member Author

Choose a reason for hiding this comment

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

The default env vars cannot be static and initialized only once, they can change.

Copy link
Member

Choose a reason for hiding this comment

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

You can still make it static and mutable so that you only have to reinitialize it when it does change, right?

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'm not sure what you mean. Are you suggesting to create a static field which contains a Dictionary<string, string> and whenever some of the inputs to this code change

{ "MSBuildExtensionsPath", MSBuildExtensionsPathTestHook ?? AppContext.BaseDirectory },
{ "MSBuildSDKsPath", GetMSBuildSDKsPath() },
{ "DOTNET_HOST_PATH", GetDotnetPath() },

we would change the dictionary accordingly? That sounds like introducing some error-prone logic for what I think would be a negligible benefit.

Also a static mutable field sounds problematic in case of multi-threaded access.

@RikkiGibson RikkiGibson self-assigned this Feb 19, 2025
@jjonescz
Copy link
Member Author

@MiYanni @RikkiGibson @chsienki for reviews, thanks

public int Execute(string[] binaryLoggerArgs, LoggerVerbosity verbosity)
{
var binaryLogger = GetBinaryLogger(binaryLoggerArgs);
var consoleLogger = new ConsoleLogger(verbosity);
Copy link
Member

Choose a reason for hiding this comment

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

nit: should use new MSBuild API from @MichalPavlik to auto-detect the use of the Console Logger vs the Terminal Logger.

Copy link
Member Author

Choose a reason for hiding this comment

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

Can you elaborate? I couldn't find such API.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member Author

@jjonescz jjonescz Mar 5, 2025

Choose a reason for hiding this comment

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

Looks like the SDK intentionally doesn't use terminal logger now, see:

// Temporary fix for 9.0.1xx. 9.0.2xx will use the TerminalLogger in the safe way.
var thing = new ConsoleLogger(msbuildVerbosity);

Nevertheless, I will change this PR to call the same method so when that's fixed, the new logic will use the same logger.

Btw, I think it's because the new API didn't flow here yet. I would rather not change that in this PR as it seems orthogonal.


<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />

<!-- Override targets which don't work with project files that are not present on disk. -->
Copy link
Member

Choose a reason for hiding this comment

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

Ok now this is some deep magic. It looks like all of this is Restore-time stuff, do we need to add some kinds of flags/detections to enable in-memory use cases for real instead of requiring these workarounds?

cc @rainersigwald / @nkolev92

Copy link
Contributor

Choose a reason for hiding this comment

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

It feels strange that an xml string is needed to make an in-memory project, when we are using the msbuild APIs directly for so many other things.

Copy link
Member Author

@jjonescz jjonescz Feb 27, 2025

Choose a reason for hiding this comment

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

It feels strange that an xml string is needed to make an in-memory project, when we are using the msbuild APIs directly for so many other things.

Isn't that same with strings containing C# that you need when using Roslyn APIs? You could create SyntaxTrees manually but no one does that. Similarly, I could create the MSBuild project without the string but seems that would be less readable.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think I am relatively comfortable with it, especially if it remains as "just a string" and we don't start using a complex interpolated string with XML escaping handling, etc. to make the project.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok now this is some deep magic. It looks like all of this is Restore-time stuff, do we need to add some kinds of flags/detections to enable in-memory use cases for real instead of requiring these workarounds?

Yes, ideally the targets would allow in-memory builds. Should I file an issue for it at https://github.com/NuGet/Home since the targets live there I think?

Copy link
Member Author

Choose a reason for hiding this comment

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

These are in obj folder right? That still exists (I imagine it would be much more difficult to get rid of that), so I don't think this should be a problem.

Copy link
Member

Choose a reason for hiding this comment

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

not quite - the big problem is that a key file in the obj folder is not 'scoped' to a single project - it's called project.assets.json. This name is customizable though, so your in-memory project here should set that property so that we're future-proof for the "multiple apps in one directory" scenario. That property name is ProjectAssetsFile.

You probably want to make sure that they synthetic project name is unique-per-app too to account for MSBuild logic that does things like $(MSBuildProjectName).foo.bar.

Copy link
Contributor

Choose a reason for hiding this comment

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

Can you help me understand why is the scoping of the file name a problem?

@jjonescz I think it's a good to consider some sort of a design review for #47020.

It involves NuGet gestures, but the NuGet team hasn't really had a chance to look into that yet.
I've tagged the appropriate people, but it's a bit of short notice for everyone to fully wrap their heads around what's going on.

Copy link
Member

Choose a reason for hiding this comment

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

@nkolev92 when I was looking at the ProjectAssetsFile logic in the targets, right next to it I saw other logic that was using the MSBuildProjectName to compute an intermediate file path - to me that implies that we need to be careful to ensure each 'app' has a distinct project name so that intermediates could reasonably be created in the same intermediate directory without clobbering

Copy link
Member Author

Choose a reason for hiding this comment

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

Scoping could be a problem if we allow multiple virtual projects in one folder, but the proposal currently says they would get separate artifacts folders, so that should avoid any problems.

short notice

I think it is expected that the implementation details of this feature might change over time, so there is no rush.


<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
Copy link
Member

Choose a reason for hiding this comment

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

Since this is in main this should be net10.0 - we should have the current TFM available as a constant that you can interpolate into this structure.

Copy link
Member

Choose a reason for hiding this comment

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

+1 to making this take into account current TF. Please don't resolve things you haven't fixed. I got here and planned to make pretty much exactly this comment.

Copy link
Member Author

@jjonescz jjonescz Mar 5, 2025

Choose a reason for hiding this comment

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

Sorry, I addressed this in a similar place in tests, didn't notice this comment was on a different place.

we should have the current TFM available as a constant that you can interpolate into this structure.

I found such variable in tests, but not in product code. However, in a follow up PR that adds "grow up" support, I'm adding a test that will verify this code matches the dotnet new template, so that should ensure we stay consistent.

I got here and planned to make pretty much exactly this comment.

I would actually prefer that, reusing old threads is hard to follow.

throw new GracefulException(LocalizableStrings.RunCommandExceptionNoProjects, directory, "--project");
if (args is not [var arg, ..] ||
string.IsNullOrWhiteSpace(arg) ||
!arg.EndsWith(".cs", StringComparison.OrdinalIgnoreCase) ||
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: IsNullOrWhiteSpace check seems redundant with EndsWith check. If it is whitespace, it will fail EndsWith check anyways. It seems reasonable to change above pattern to args is not [string arg, ..] then go straight to !arg.EndsWith(...).

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks, the EndsWith was added later and I didn't realize I could simplify the previous code.

Copy link
Member

Choose a reason for hiding this comment

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

Just for my clarification, args is not [{ } arg, ..] means:

  • For this predicate to return true, args contains zero values (has no first value) or the first value is null.
  • Therefore, when this predicate returns false, args contained at least 1 value and the first value is non-null.
  • Additionally, since this is used in an || context, arg will exist in subsequent predicates when this predicate is false.

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, let me know if I should clarify that somehow.


<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />

<!-- Override targets which don't work with project files that are not present on disk. -->
Copy link
Contributor

Choose a reason for hiding this comment

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

It feels strange that an xml string is needed to make an in-memory project, when we are using the msbuild APIs directly for so many other things.


private ProjectRootElement CreateProjectRootElement(ProjectCollection projectCollection)
{
var projectFileFullPath = Path.ChangeExtension(EntryPointFileFullPath, ".csproj");
Copy link
Contributor

Choose a reason for hiding this comment

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

So this causes us to use the containing directory of file.cs to gather up .cs files to include in the project? Is that right?

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, that's correct, the "implicit" or "virtual" project conceptually lives at the same level as the executed file.

Copy link
Member

Choose a reason for hiding this comment

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

This doesn't seem desirable. If you're specifically trying to build one file, there could easily be bugs (including compile-time bugs) that would prevent you from dotnet run'ning this file.

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'm not sure I follow. Are you suggesting dotnet run file.cs should only consider file.cs and not the other files in the directory? That's surely a possibility but more of a design question (see also the design doc at #47020). In short, the current design envisions users having multiple "scripts" in one directory like scripts/restore.cs, scripts/build.cs and some shared utilities like scripts/utils.cs which should be accessible from the scripts.


<!--
Override targets which don't work with project files that are not present on disk.
See https://github.com/NuGet/Home/issues/14148.
Copy link
Contributor

@nkolev92 nkolev92 Feb 27, 2025

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

static graph is not ever going to be a thing with this loose-file usage mode.

Copy link
Contributor

Choose a reason for hiding this comment

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

Static graph restore never triggers any of the target being reported as a problem, so I thought it'd just skip the problem.
Why do we have a concern with static graph?

Copy link
Contributor

Choose a reason for hiding this comment

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

Just wanted to make sure you saw, why is static graph restore a concern?

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 sorry, I thought you were asking what if user enabled static graph restore, whether this will work. But you are saying we should enable static graph restore and then perhaps we could avoid this workaround. Thanks, I will investigate that.

Copy link
Contributor

Choose a reason for hiding this comment

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

  1. I'd be curious to see the error that's happening and where it's getting thrown from.
    These targets are not invoked at all during static graph restore, so it's surprising if the error is the same.
  2. There's no functional differences between the 2. The biggest reason why static graph is not default is because it's slower for smaller projects because of the extra process.
  3. What are we saying is not supported?

Copy link
Member Author

Choose a reason for hiding this comment

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

  1. I'd be curious to see the error that's happening and where it's getting thrown from.
    These targets are not invoked at all during static graph restore, so it's surprising if the error is the same.

The error is not exactly the same, let me try to figure out where it's coming from.

3. What are we saying is not supported?

Static graph restore in combination with dotnet run file.cs.

Copy link
Contributor

@nkolev92 nkolev92 Mar 5, 2025

Choose a reason for hiding this comment

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

I might be misunderstanding this scenario, but given that customers don't see a project file, I don't think they care about static graph vs regular restore, they just care that it gets compiled and run.

So I don't think we have a support story for static graph there.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure, but they could do dotnet run file.cs -p:RestoreUseStaticGraphEvaluation=true or more likely have RestoreUseStaticGraphEvaluation set in a parent Directory.Build.props and then if such dotnet run file.cs fails that seems like a bug (unless we say it's unsupported for starters).

Copy link
Member Author

@jjonescz jjonescz Mar 5, 2025

Choose a reason for hiding this comment

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

So the error comes from RestoreTaskEx, not sure where exactly but found this test that shows the exact error:

https://github.com/NuGet/NuGet.Client/blob/54f4fd0b4b418666683ab717d075fe0a239eb484/test/NuGet.Core.FuncTests/Msbuild.Integration.Test/MsbuildRestoreTaskTests.cs#L821

The error is error MSB4025: The project file could not be loaded. Could not find file '...'.

For repro (which produces a binlog for example), you can use this branch: https://github.com/jjonescz/MSBuildInMemory/tree/static-graph

@baronfel baronfel added Area-run-file Items related to the "dotnet run <file>" effort and removed untriaged Request triage from a team member Area-CLI labels Feb 27, 2025
Comment on lines 149 to 154
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Copy link

Choose a reason for hiding this comment

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

Should we enable AllowUnsafeBlocks by default?

Copy link
Member

Choose a reason for hiding this comment

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

Think we should keep the current state where unsafe requires a specific build opt in. Overall we want to have less unsafe code in the ecosystem, not more.

Copy link
Member Author

Choose a reason for hiding this comment

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

In general, the dotnet run file.cs feature aims to use the default project template, whatever that is. That makes it predictable.

Copy link
Member

Choose a reason for hiding this comment

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

If that's true, then why did you change the default template?

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 made minimal changes to workaround bugs that prevent it building without a physical file on disk. There is a tracking issue linked and ideally that will be fixed and the workaround can be removed and the template will exactly match the default one.

@hez2010
Copy link

hez2010 commented Mar 4, 2025

I believe the most usage would be scripting with shebang syntax. Should we run it with Release config by default to avoid unnecessary by-default slow down?

@jjonescz
Copy link
Member Author

jjonescz commented Mar 4, 2025

I believe the most usage would be scripting with shebang syntax. Should we run it with Release config by default to avoid unnecessary by-default slow down?

I would rather not change any defaults to avoid confusion. However, if you are using shebang, presumably you will specify the Release config there:

#!/usr/bin/dotnet run -c Release

Environment.SetEnvironmentVariable(key, value);
}

// Setup MSBuild.
Copy link
Member

Choose a reason for hiding this comment

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

tiny nit:

Suggested change
// Setup MSBuild.
// Set up MSBuild.

return arg.StartsWith("/bl:", comp) || arg.Equals("/bl", comp)
|| arg.StartsWith("--binaryLogger:", comp) || arg.Equals("--binaryLogger", comp)
|| arg.StartsWith("-bl:", comp) || arg.Equals("-bl", comp);
}
Copy link
Member

Choose a reason for hiding this comment

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

|| arg.StartsWith("/binaryLogger:") || arg.Equals("/binaryLogger")

Casing?

Copy link
Member

Choose a reason for hiding this comment

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

For casing, it uses StringComparison.OrdinalIgnoreCase.

Copy link
Member

Choose a reason for hiding this comment

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

Completely missed that 👍

consoleLogger.Shutdown();
}

static ImmutableArray<ILogger> GetBinaryLoggers(string[] args)
Copy link
Member

Choose a reason for hiding this comment

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

There isn't an error for having multiple binlog arguments, but it doesn't actually make a bunch of different binlogs.

image

This should reflect that.

Copy link
Member Author

@jjonescz jjonescz Mar 5, 2025

Choose a reason for hiding this comment

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

Good catch, thanks, will fix. It looks like there are some inconsistencies though - dotnet-run binlogs are created for each -bl: argument, see this function:

static FacadeLogger? DetermineBinlogger(string[] restoreArgs)

I will change that as well to make this all consistent.

Copy link
Member

Choose a reason for hiding this comment

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

Sounds good. I'm not sure why someone would even want multiple binloggers attached to a single build. I can imagine wanting a separate binlog for different builds that happened as part of some script if they would overwrite each other, but making that FacadeLogger just seems wrong to me.


// Then do a build.
var buildRequest = new BuildRequestData(
CreateProjectInstance(projectCollection),
Copy link
Member

Choose a reason for hiding this comment

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

Why are we creating two separate project instances?

Copy link
Member Author

@jjonescz jjonescz Mar 5, 2025

Choose a reason for hiding this comment

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

Because we need to set additional global properties in the restore vs the build. As far as I understand MSBuild APIs, this is the only way to do it.

Copy link
Member

Choose a reason for hiding this comment

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

BuildRequestData has an overload with global properties

Copy link
Member Author

Choose a reason for hiding this comment

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

BuildRequestData has an overload with global properties

Those ones only take physical file paths though, right? (And they read the XML from the path passed in.) I need to use a BuildRequestData overload that takes the XML data because my project file doesn't exist on disk.

Copy link
Member

Choose a reason for hiding this comment

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

Hmm...I think you're right. You could consider adding a new API for that to MSBuild, since that seems like it would be very easy to implement, but I'd consider that a v3 change not necessary for this PR.

// Do a restore first (equivalent to MSBuild's "implicit restore", i.e., `/restore`).
// See https://github.com/dotnet/msbuild/blob/a1c2e7402ef0abe36bf493e395b04dd2cb1b3540/src/MSBuild/XMake.cs#L1838
// and https://github.com/dotnet/msbuild/issues/11519.
var restoreRequest = new BuildRequestData(
Copy link
Member

Choose a reason for hiding this comment

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

Instead of doing any of this, why can't we make a fake project then "call" RunCommand.Execute (with modified parameters) on it? That would make me feel much more comfortable.

Copy link
Member Author

@jjonescz jjonescz Mar 5, 2025

Choose a reason for hiding this comment

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

  1. Performance is presumably better by invoking MSBuild in memory. We are planning to do more optimizations as well (see the design doc), e.g., skipping MSBuild invocation and invoking just csc.exe or even just the target dll. For that, we will need to communicate with MSBuild to determine the csc arguments.
  2. If we create a fake project, it will show up on disk during the build time, which is not a great user experience. We could create it inside obj folder but that's a problem because then it has a different path which is an observable difference.
  3. Hopefully, MSBuild will introduce a public API for implicit restore (see the linked tracking issue) and we can drastically simplify this.

Copy link
Member

Choose a reason for hiding this comment

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

When I wrote that comment, I'd glanced only very briefly at RunCommand, and many of its methods operated on ProjectInstances, so my question was whether you could create a ProjectInstance and pass it to RunCommand.

I looked further just now, and though it does a lot of checks with ProjectInstances, it ultimately creates an MSBuildForwardingApp that calls MSBuild's Main method directly, and there's no overload for that that takes in a ProjectInstance.

I will caution against worrying too much about performance at this stage. RunCommand does a lot of work that you don't, and I'd rather this be correct than fast. If this is correct, why is RunCommand bulky? Could RunCommand be cut down and perhaps even made more performant if it were to follow a model similar to yours?

Copy link
Member Author

@jjonescz jjonescz Mar 6, 2025

Choose a reason for hiding this comment

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

As far as I understand, RunCommand only uses ProjectInstances and MSBuild APIs to get the final executable path and RunArguments (users can use that to override how their project actually runs, e.g., what command line arguments are passed to it). This PR reuses all that. For the build part, RunCommand just calls MSBuild's Main method. We cannot reuse that, there is no way to pass XML project file text to MSBuild via its Main method as far as I know.

Copy link
Member Author

@jjonescz jjonescz Mar 6, 2025

Choose a reason for hiding this comment

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

RunCommand does a lot of work that you don't

Can you elaborate on that? I'm not sure what work RunCommand does that this PR doesn't.

Note that this PR just hooks into the "build" stage of RunCommand. The rest (that executes the built DLL) is still the same.

Copy link
Member

Choose a reason for hiding this comment

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

I see:

  • Launch settings (which we aren't supporting here intentionally)
  • Clean cancelling on ctrl-c
  • Exception handling
  • Restore arguments? (nologo, verbosity; I'm not sure we need this)
  • User-passed properties/targets (I think there's a strong argument that we shouldn't support this)

So maybe just cleanup on ctrl-c and exceptions? For exceptions, I saw that you had the catch (Exception e) bit below; in the SDK, we normally try to get a more precise exception that includes e.Message as part of a GracefulException rather than being the whole thing printed. This isn't a huge thing in my mind, so if you want to defer it for v2, I'm fine with that.


private ProjectRootElement CreateProjectRootElement(ProjectCollection projectCollection)
{
var projectFileFullPath = Path.ChangeExtension(EntryPointFileFullPath, ".csproj");
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't seem desirable. If you're specifically trying to build one file, there could easily be bugs (including compile-time bugs) that would prevent you from dotnet run'ning this file.

{
var projectFileFullPath = Path.ChangeExtension(EntryPointFileFullPath, ".csproj");
var projectFileText = """
<Project>
Copy link
Member

Choose a reason for hiding this comment

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

Why aren't you just specifying Sdk="Microsoft.NET.Sdk" in the Project element?

Copy link
Member Author

Choose a reason for hiding this comment

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

Because the target overrides wouldn't have an effect - the Sdk targets would be implicitly imported after the custom targets, i.e., the Sdk targets would overwrite the custom ones, but we want that the other way around.

Copy link
Member

Choose a reason for hiding this comment

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

add a comment so that's clear?


<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
Copy link
Member

Choose a reason for hiding this comment

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

+1 to making this take into account current TF. Please don't resolve things you haven't fixed. I got here and planned to make pretty much exactly this comment.

<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
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 convinced we should force things like nullable. You can consider making it an option, but this is doing something simple, and forcing people to be perfect on this sort of thing will just be annoying.

Copy link
Member

Choose a reason for hiding this comment

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

We intend to match the default experience for console projects and present what we consider idiomatic defaults for modern C#, which means nullable is enabled. If people want to disable it, they can use the directive.

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, dotnet run file.cs is intended be the "default" experience. For anything else, it is recommended to migrate to project-based programs or use directives if they are available.

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 find this argument convincing.

Who wants nullability? People who work on large, complex C# projects with numerous developers.

Who doesn't want nullability? People trying to do something quickly where they don't care about the general case, only their specific case, and people who are trying to learn.

In particular, I still remember that when I was learning C#, if I saw an error message, I didn't really look at it; I just looked at my code to try to see what was wrong and added print statements if necessary. That's ok in some languages (albeit very suboptimal for C#), and it doesn't work at all for nullable.

This isn't a hill I'm willing to die on, but I do think we should be very conscious of the effect of Nullable in a context like this.

Copy link
Member Author

@jjonescz jjonescz Mar 6, 2025

Choose a reason for hiding this comment

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

Perhaps you are right, but why we have <nullable>enable</nullable> in project file templates then? Today, they are the starting point for users learning C#. Advanced users could presumably enable that on their own. That's also why dotnet run file.cs just reuses what dotnet new does - there shouldn't be much difference IMO, both are used as "the starting point".

Consider also that nullability is very difficult and error-prone to add later on, it's best to have it from the start.

Nullability is also by default just warnings, not errors, and users starting with C# could definitely ignore them.

Despite all that, I wouldn't mind not enabling nullability by default for file-based programs, that's completely valid design decision. But it seems like a big shift where we would be saying "nullability is only for advanced users" but I hear that for the first time, my feeling is that we generally recommend users to always use nullability for their new projects.

Copy link
Member

Choose a reason for hiding this comment

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

I think the best reason for enabling nullable in our templates is just to build awareness.

I'm mildly concerned that people who look up the nullable warnings they'd get would be told to disable it in their project files and get confused by that—but I hadn't considered that they're warnings by default. To be honest, I'm working on a Java project at the moment, and I've been consistently ignoring some of the warnings the compiler keeps telling me about—but it isn't intended to scale, so it isn't a problem.

With that in mind, I think I'd downgrade how much I care about this to only a mild preference for nullable disabled. Since it sounds like others have expressed preferences for it to stay in, I don't think this needs to change.

Comment on lines 149 to 154
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Copy link
Member

Choose a reason for hiding this comment

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

If that's true, then why did you change the default template?

return arg.StartsWith("/bl:", comp) || arg.Equals("/bl", comp)
|| arg.StartsWith("--binaryLogger:", comp) || arg.Equals("--binaryLogger", comp)
|| arg.StartsWith("-bl:", comp) || arg.Equals("-bl", comp);
}
Copy link
Member

Choose a reason for hiding this comment

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

For casing, it uses StringComparison.OrdinalIgnoreCase.

throw new GracefulException(LocalizableStrings.RunCommandExceptionNoProjects, directory, "--project");
if (args is not [var arg, ..] ||
string.IsNullOrWhiteSpace(arg) ||
!arg.EndsWith(".cs", StringComparison.OrdinalIgnoreCase) ||
Copy link
Member

Choose a reason for hiding this comment

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

Just for my clarification, args is not [{ } arg, ..] means:

  • For this predicate to return true, args contains zero values (has no first value) or the first value is null.
  • Therefore, when this predicate returns false, args contained at least 1 value and the first value is non-null.
  • Additionally, since this is used in an || context, arg will exist in subsequent predicates when this predicate is false.

Copy link
Member

@Forgind Forgind left a comment

Choose a reason for hiding this comment

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

I heard offline that the new plan is to have it just build/execute one file instead of globbing everything in the current directory and subdirectories. That's my last serious concern right now, so I'm happy signing off preemptively 🙂 Thanks for all your work on this! I think it's a fantastic improvement in the new user experience

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-run-file Items related to the "dotnet run <file>" effort
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants