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 Simple C# Programs #213

Closed
wants to merge 8 commits into from
Closed

Conversation

cartermp
Copy link

@cartermp cartermp commented May 1, 2021

Let's see if we can make C# a little simpler.

Working list of additions/changes to make to the doc:

  • Clarify things like build output
  • Correct mistakes/assumptions
  • Call out primary affected areas
  • Propose an order of work
  • Call out tooling scenarios
  • More attributions (e.g., dotnet-script)

Co-authored-by: Fred Silberberg <fred@silberberg.xyz>
@gulshan
Copy link

gulshan commented May 3, 2021

Do the commands dotnet run and dotnet build without filename param assume only a single C# file in the directory? I think, having multiple C# files in a directory will be more common and running them using their file names will be more useful. And if loading one file from another is supported, this determines which file will be the entry-point, without guessing by dotnet tooling.

Also, is there a chance to evolve CSX to meet these introductory/scripting demands, while keeping C# structured? Because when I see a .fs and a .fsx file, I know they behave differently, and they are meant to be used differently. There is no cognitive load of getting the context to really know how the file will behave. Where #load or #r "nuget..." can and cannot be used, no unlearning needed. Just my opinion though. I don't know much about the obstacles here.

@cartermp cartermp marked this pull request as ready for review May 3, 2021 21:40
@KevinCathcart
Copy link

It is also worth pointing out that #r and #load are part of the language, even if currently only supported in scripting scenarios. Even if this is a new third mode (new SourceCodeKind member in the compiler) somewhere between scripting and traditional, it makes sense to still reuse these directives.

Furthermore it makes sense to have the compiler enforce the exact same semantics for these directives as for c# scripting. If new features like framework references are needed here, then scripting could benefit from them too. There are already requests for scripting (the official csi.exe and/or VS interactive window) to support the same nuget package reference syntax that dotnet-script and .NET Interactive support (see dotnet/roslyn#43918 for one such request).

Other than this needing to be something the host deals with resolving (the compiler could certainly parse this for the host's benefit, but I doubt the compiler wants to be in charge of actually fetching the nuget packages) I see no reason this should not be supported everywhere the #r syntax is allowed.

I do have mixed feeling about introducing a third type of csharp mode for this. The simple program language support has already made the scripting and non-scripting a lot more similar than they previously were, yet the scripting language has some "features" that may be undesirable for simple program usage.

On the other hand. the whole point here seems to be making it possible to run these files a lot like an interpreted scripting language (python, ruby, etc), including hash bang support, which kind of points in the direction that this really should just be using the scripting language despite its differences from full c#, or perhaps have this be some form of stricter scripting mode that disables some of the undesirable differences.

@cartermp
Copy link
Author

cartermp commented May 4, 2021

@KevinCathcart A key thing, which perhaps isn't expressed well in this document, is that this is not another dialect of C# like the scripting dialect is. It's just C#. Everything here (top-level statements, global using, hashbang support) is proposed as simply just being C# proper. There's no "simple mode" activated or anything. If you have these in a C# file in a project-based codebase, nothing behaves differently.

There's some more details to write up here, such as also allowing #r "path-to-assembly" (no reason for that not to work) and re-using #load provided that multiple files are supported.

This is in contrast to the scripting dialect, which very much changes C# semantics such as having shadowing and disallowing namespace declarations. This proposal is not about unifying the scripting dialect with C# proper. It's just about pulling in existing and proposed C# features + updating build tooling to allow using them together without a project.

@mhutch
Copy link
Member

mhutch commented May 4, 2021

Making #r "nuget:[package]" work correctly in a project context would be difficult as you'd have to reconcile the C# compiler's #r package graph with the assets already resolved by NuGet restore, or somehow pull the #r "nuget:[package]" references out and handle them in the existing NuGet restore. Similarly you'd have to reconcile #r "[dl]" assembly references and RAR.

@cartermp
Copy link
Author

cartermp commented May 4, 2021

Yeah, in a project context they'd likely get ignored, at least that's the current thinking. Any #r or #load would just be ignored. But it's worth thinking about more

@zivkan
Copy link
Member

zivkan commented May 4, 2021

Some packages, for example SQLite, need to run msbuild props/targets to work. This is most common for packages that are managed wrappers of native dlls, and the packages support both packages.config as well as PackageReference, hence why they contain msbuild props/targets and not just use NuGet's runtimes/ folder.

Maybe this question is too detailed for a high level design, but should these packages work with the single file #r syntax? Only support packages that do not have msbuild props/targets and warn/error that the customer must use a project file to use the package?

@cartermp
Copy link
Author

cartermp commented May 4, 2021

@zivkan that's a good point of consideration. I haven't dived into details like this just yet (maybe that comes later?) but in my head I imagine that an in-memory or temp project file is involved. For example, here's that package working in F# Interactive, which has #r "nuget:...". It's actually the same mechanism that is used in .NET Interactive:

image

If a project file is generated behind the scenes (temp or in memory) then other msbuildy things could (in theory?) also work, like .editorconfig plumbing into the compiler, directory.build.props, etc.

@KevinCathcart
Copy link

@KevinCathcart A key thing, which perhaps isn't expressed well in this document, is that this is not another dialect of C# like the scripting dialect is. It's just C#. Everything here (top-level statements, global using, hashbang support) is proposed as simply just being C# proper. There's no "simple mode" activated or anything. If you have these in a C# file in a project-based codebase, nothing behaves differently.

So you are proposing removing the errors for those directives for normal project based compilation to avoid even being a minor dialect from the language POV? (arguable still a minor dialect none-the-less, since you are applying different meaning to those directives then I would get by typing csc.exe filename.cs {/reference:...} but fair enough.)

I don't see any discussion of possibly allowing no-op or fully functional #r or #load directive in normal compilation in the LDM notes, so I'm guessing they have not weighed in on if doing that is sensible? Obviously it is trivially possible to implement, but that does not mean it is a good idea.

@333fred
Copy link
Member

333fred commented May 5, 2021

I don't see any discussion of possibly allowing no-op or fully functional #r or #load directive in normal compilation in the LDM notes, so I'm guessing they have not weighed in on if doing that is sensible?

It's tentatively on the schedule for next week. Suffice to say that we have a good deal of interest in the idea, as we've been discussing it in various forms (mostly via the top-level statements feature) for a few years now. https://github.com/dotnet/csharplang/tree/main/meetings/2021#may-12-2021


## `dotnet` command support

Only `dotnet run` and `dotnet build` are supported with Simple C# Programs. All other `dotnet` commands will error out, indicating that you need a project.
Copy link
Member

Choose a reason for hiding this comment

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

dotnet test would be interesting too. The file could have top-level methods that are unit tests, and dotnet test would run them.

While working in JavaScript, I found standalone unit test files useful when sharing a minimal repro for a library bug--that's something especially hard to do with .NET today.

@KevinCathcart
Copy link

I don't see any discussion of possibly allowing no-op or fully functional #r or #load directive in normal compilation in the LDM notes, so I'm guessing they have not weighed in on if doing that is sensible?

It's tentatively on the schedule for next week. Suffice to say that we have a good deal of interest in the idea, as we've been discussing it in various forms (mostly via the top-level statements feature) for a few years now. https://github.com/dotnet/csharplang/tree/main/meetings/2021#may-12-2021

Cool. So the question of if this should effectively be a super minor dialect, or if #r (and possibly #load) should be supported in project based builds is yet to actually be settled. Here are some things that probably ought to be discussed at LDM:

  • If those features act as more than no-ops in a project-based build, these would need to be reported back to MSBuild, so it could write them to a cache file. Otherwise MSBuild has no way of doing proper incremental builds, since these would be new inputs it did not know needed to be checked for updates. Off the top of my head, I'm not sure of any other case where the compiler ends up using input files that MSBuild did not explicitly specify, but there could already be such cases I am simply not aware of. There are also project system impacts, etc.
  • If they do act as no-ops, then it could be really confusing to have them silently be ignored in project based compilation. They could issue a warning or error message in this case. Issuing an error message would be creating a new minor dialect at the language level, while the warning message would not. Obviously people who have enabled warnings as errors would error in this case. However they would be transitioning from an error message that the directives are only supported by scripting mode into a warning promoted to error that the directives are having no effect, so that is unlikely to be an issue.
  • There are other open questions. Is something like .NET Interactive's #i directive to specify additional nuget feeds needed? Or is just requiring a nuget.config in the same folder fine? Unlike nuget based #r which is supported by both dotnet-script and .NET Interactive, this one looks to be exclusive to interactive, as are the various pesudo-hashbang directives that interactive supports.

I also note that .NET Interactive does not currently rely on the compiler parsing the NuGet version of #r directives, but instead preprocesses them, letting the non-NuGet cases fall through and be processed by the compiler. In the case of C# I'm not sure this behavior is strictly required for the simple case, and a custom MetadataReferenceResolver could probably handle things, but .NET Interactive supports these directives for thinks like PowerShell which don't have any native support for such directives, so it needs to do this as a preprocessing step for them.

Preprocessing is probably best avoided, since making that work as expected with things like ifdefs is really tricky. On the other hand, for nuget packages that include MSBuild snippets that are required for proper functioning, loading them in the compiler is potentially too late. I'm also not sure if packages including analyzers or source generators would be a problem during compilation, or if those could be successfully added to a compilation already in progress,

@333fred
Copy link
Member

333fred commented May 5, 2021

So the question of if this should effectively be a super minor dialect, or if #r (and possibly #load) should be supported in project based builds is yet to actually be settled

Well, it's been a key driving principle that it's not a dialect of C#. Also, while it's certainly up for discussion, I don't believe anyone has discussed or has intentions of putting a NuGet resolver into the compiler itself: it will almost certainly be a separate tool invoked as part of the pipeline. Whether that tool has differences when invoked as part of a project file or when just running a .cs file certainly still needs to be designed, but I don't expect roslyn itself to know anything about whether it's just compiling a .cs file or compiling a traditional .csproj project.

@RichiCoder1
Copy link

RichiCoder1 commented May 6, 2021

@zivkan that's a good point of consideration. I haven't dived into details like this just yet (maybe that comes later?) but in my head I imagine that an in-memory or temp project file is involved. For example, here's that package working in F# Interactive, which has #r "nuget:...". It's actually the same mechanism that is used in .NET Interactive:
...snip...
If a project file is generated behind the scenes (temp or in memory) then other msbuildy things could (in theory?) also work, like .editorconfig plumbing into the compiler, directory.build.props, etc.

Would a net effect of this also allow something like Source Generators? I could see some interesting applications specifically for using source generators specifically with simple programs.

@aelij
Copy link

aelij commented May 6, 2021

in a project context they'd likely get ignored, at least that's the current thinking. Any #r or #load would just be ignored. But it's worth thinking about more

Why not support #r in project contexts as well? It would make .cs files more encapsulated, easily movable between projects, and avoids manually adding usings. Go does something similar with import where you can just specify remote and local packages.

Copy link

@smoothdeveloper smoothdeveloper left a comment

Choose a reason for hiding this comment

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

Adding a section about C# compiler error messages, and the expectation for them to be more helpful in context without tooling would also be great.

Some minor comments along the lines.

proposed/simple-csharp-programs.md Show resolved Hide resolved
proposed/simple-csharp-programs.md Show resolved Hide resolved
proposed/simple-csharp-programs.md Show resolved Hide resolved
proposed/simple-csharp-programs.md Show resolved Hide resolved
proposed/simple-csharp-programs.md Outdated Show resolved Hide resolved
proposed/simple-csharp-programs.md Show resolved Hide resolved
@jmarolf
Copy link

jmarolf commented May 6, 2021

I haven't dived into details like this just yet (maybe that comes later?) but in my head I imagine that an in-memory or temp project file is involved.

imho the semantics should be similar to how AssemblyInfo.cs works: a default is generated for you, but you can provide your own if you need to customize it. So if the only thing I need to customize in my project file is:

  • target framework
  • set of nuget packages

Then you don't need a project file and those can be inferred for the C# files themselves.

This would have the consequence that:

dotnet build <some folder>

would act as if it were running as

dotnet build <some folder>\<some folder>.csproj

(assuming only C# files are in that folder, I assume this should error if there are cs/vb/f# files in a directory, same situation as when you run dotnet build in a folder with multiple project files.).

This also has several behavioral fallouts:

  • Directory.Build.props would "Just work" in these projects
  • editorconfig and globalconfig files would as well (though editorconfig will likely work regardless as it is folder-based)

I think the advantage of these semantics is it is easy to explain when to use a project file:

  • Do you need some custom build or pack logic? Ok you need a project file to specify those things.
  • Is everything you are doing just the defaults or are all your common settings coming from Directory.Build.props? No need for a project file just run dotnet <command> on that folder

NOTE: I am also assuming that any dotnet cli command that operates on projects today would work following this scheme. That would mean:

  • dotnet add
  • dotnet build
  • dotnet build-server
  • dotnet clean
  • dotnet msbuild
  • dotnet pack
  • dotnet publish
  • dotnet remove
  • dotnet restore
  • dotnet run
  • dotnet test
  • dotnet vstest

@zivkan
Copy link
Member

zivkan commented May 8, 2021

I am also assuming that any dotnet cli command that operates on projects today would work following this scheme.

dotnet add reference, dotnet remove reference are used for project (p2p) references. If we're going to support p2p references, then this is no longer just about "simple programs" (yes, in my opinion a program that spans multiple assembly is no longer simple). The spec explicitly calls out that programs can "grow up" (a name which I don't like at all, FWIW), so I personally think it's completely reasonable to not support all dotnet cli commands. Otherwise we're effectively trying to make project files obsolete.

For the same reason, I wouldn't expect dotnet msbuild to work with simple programs.

While dotnet new classlib; dotnet pack does work, high quality packages should have additional metadata, currently only specified in the project file. Furthermore, I associate "program" with "exe", so "simple programs" to me does not mean "simple libraries" or "simple packages", so I don't make the assumption that dotnet pack should work in a directory with only program.cs.

It seems to me the ideal/preference of making all dotnet cli commands work consistently for directories without project files does not line up with the spec's proposal that programs will need to "grow up" to have project files. Otherwise I think this spec should be proposing that project files become optional/obsolete, rather than being only for simple programs.

Copy link
Member

@jcouv jcouv left a comment

Choose a reason for hiding this comment

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

Done with review pass (iteration 6)


Behind the scenes, a likely implementation will involved a synethesized project file. This would then have all of the information to make the `dotnet` commands work, and open us up to allowing more `dotnet` commands in the future.

A synthezized project file could live either in memory or in the temp folder.
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 sure that'll come up in next round of discussion with SDK team: we'll want to specify what that project looks like.
I assume the OutputType would be Exe, for instance.

```csharp
#r "nuget: Newtonsoft.Json"

record Person(string Name);
Copy link
Member

Choose a reason for hiding this comment

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

nit: As you mentioned, the record declaration should come after the top-level statements.

Co-authored-by: Fred Silberberg <fred@silberberg.xyz>
@cheong00
Copy link

For me, I think another possibility is to put it like those .NET v2 "website project", where we have a .CONFIG file provide configurations common to files in folder, all .aspx/,asmx files can be treated as single application, and you can freely reference any addition binaries in bin folder.

File location and config file syntax could be rearranged it to make it configuration folder like .git, possibly with simple configuration UI like that of TortoiseGit or IIS management for those less comfortable with editing config files directly.

Now when I work on .CS files inside that folder, I can be sure that the basic settings I used to compile to be the same and I don't need to re-specify it every time.

@jmarolf
Copy link

jmarolf commented May 16, 2021

@zivkan

If we're going to support p2p references, then this is no longer just about "simple programs" (yes, in my opinion a program that spans multiple assembly is no longer simple).

There is no technical reason the #r or #load syntax couldn't be updated to support project references. But supposing we do not allow this, from a tooling perspective, how do you see users adding new packages? Do they get completion in their code file within #r?

I think it is good to have the discussion about where the "line" is terms of what is supported. I took a position very much on the "most possible things working" side because it is easy to explain. What works? Everything. My internal model is "If you don't need project files you should not require them". Project to project references very much feel like a case where having project files makes things easier. Perhaps a project-less program could reference other projects, I could see the utility of this. But regardless of the technical hurdles I think trying to reference a folder or file instead of a direct project would be error prone. imho we should allow p2p references so long as it is not ambiguous. So a single file referencing projects would be fine.

so I personally think it's completely reasonable to not support all dotnet cli commands. Otherwise we're effectively trying to make project files obsolete.

I don't think the goal is to make them obsolete, just unnecessary for "simple" cases. You used to need to specify AssemblyInfo.cs files, now those are generated for you. You used to need to create Main methods in C#, now those are implicit. The entire re-design around project files for .NET was about not requiring you to specify the defaults over and over. This just feels akin to that effort. It will be easier to scaffold assemblies and their relationships to each other via project files. Users will migrate to them when they have a need. But they shouldn't be forced to use them if they offer no benefit (which is the same logic used for the features listed above).

While dotnet new classlib; dotnet pack does work, high quality packages should have additional metadata, currently only specified in the project file.

Yes this is a good example. I could imagine want to dotnet new + make a few changes + dotnet pack to test/prototype something. But would eventually add a project file if I was every going to ship the prototype.

Furthermore, I associate "program" with "exe", so "simple programs" to me does not mean "simple libraries" or "simple packages", so I don't make the assumption that dotnet pack should work in a directory with only program.cs.

My question is how do we tool this so it isn't confusing to users? Why is it more useful for this feature if its only console apps? I agree class libraries working is not very useful. But I assume that users won't do things that are not useful. There is no need to build a wall when a simple guardrail is sufficient. If I have a small file that I just want to ship as a library for a few of my hobby projects why can't dotnet pack work on it?

It seems to me the ideal/preference of making all dotnet cli commands work consistently for directories without project files does not line up with the spec's proposal that programs will need to "grow up" to have project files. Otherwise I think this spec should be proposing that project files become optional/obsolete, rather than being only for simple programs.

@cartermp can chime in here with his own thoughts. To me "grow up" means "having project files is useful". If having a project file is just a cognitive burden, then I argue that it should be allowed to be omitted. Project files provide real value in many situations but requiring them seems unnecessary. If your code does not require a project file then it is, in my definition, "simple".

@cartermp
Copy link
Author

cartermp commented Dec 1, 2022

I'll close this one since I don't work on the product anymore and, although I'd like to see this get done, I don't have the time nor energy to see this proposal through to completion. Anyone else who's interested can give it a whirl, though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Language/design
Development

Successfully merging this pull request may close these issues.