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

Blazor unified project design #49079

Closed
SteveSandersonMS opened this issue Jun 28, 2023 · 68 comments
Closed

Blazor unified project design #49079

SteveSandersonMS opened this issue Jun 28, 2023 · 68 comments
Labels
area-blazor Includes: Blazor, Razor Components design-proposal This issue represents a design proposal for a different issue, linked in the description feature-blazor-single-project feature-full-stack-web-ui Full stack web UI with Blazor ✔️ Resolution: Answered Resolved because the question asked by the original author has been answered. Status: Resolved
Milestone

Comments

@SteveSandersonMS
Copy link
Member

SteveSandersonMS commented Jun 28, 2023

In .NET 8 we plan to add a new project template, Blazor Web Application, that covers all combinations of server-hosted projects (traditional Blazor Server apps, Blazor WebAssembly hosted, and the new unified architecture that allows use of Server, WebAssembly, and SSR in a single project). It will work by multitargeting over net8.0 and net8.0-browser.

However we still have to decide on the conventions for how such a project should be structured. The biggest question is what determines which files/references are included in which compilation?

Goals

  • Make it obvious what ends up in which compilation. In particular, avoid unintended inclusion in the WebAssembly compilation, because (1) it bloats the app size, and (2) in the worst case, a developer might include sensitive business logic/secrets without realising it.
  • Stay compatible with existing ASP.NET Core conventions. We don't want to break all existing docs/tutorials/etc for MVC, gRPC, auth, EF, and so on. Nor do we want people upgrading an existing nontrivial ASP.NET Core app to have to make a lot of changes to their existing code.
  • Maximize clarity; minimize the concept count. Try to make the structure intuitively obvious and pleasant for people who don't know why all this stuff works as it does. In particular, be very careful about terminology given the amount of ambiguity (e.g., client vs browser vs WebAssembly all might seem to mean the same thing here, but all of them also seem wrong in some cases).
  • Work without tying people to particular project structures. We're not designing just for the default templates; we're looking for a system that will make sense even when people change their project structures a lot, or are upgrading existing real-world projects that involve a lot of unrelated concepts they might not even fully understand.

Possible designs

I think there are two three main approaches to pick from, as depicted here (click to expand):

image

Benefits of approach A ("single project, exclude by default")

  • Does not break existing ASP.NET Core server apps. Upgrading is easy, because nothing goes into your WebAssembly compilation by default, so it's not going to give you hundreds of build errors and force you to rename files or add #if or special exclusions to your .csproj.
  • As you add more components/classes to the project, you're forced to make an explicit choice if you want to make it available to WebAssembly. There is minimal risk of accidental disclosure, and the trimmed wasm bundle will be as minimal as possible.

Its main drawbacks are that the concept of ClientShared is nonobvious (and I spent ages coming up with that name, as almost everything else fails to communicate the idea that you're making stuff available to both client and server whereas otherwise it's server only - better name suggestions are welcome, but don't just say "client" or similar).

Benefits of approach B ("single project, include by default")

  • Most similar to MAUI
    • We could even have Platforms/Browser but that's largely pointless since for almost everything you include in browser, you also want it to be available in server (otherwise, for example, you can't even route to @page components that aren't in the server build and would get 404s).
    • We could have Platforms/Server but again that's quite bad because people don't want to restructure their ASP.NET Core projects to move everything for the server into that subdir.
  • Looks simpler, because there are fewer folders
  • More obvious that what determines each component's render mode is @rendermode and not which folder it is in

Its main drawback is that it is incompatible with typical ASP.NET Core projects, at least until developers manually exclude everything that can't work in WebAssembly, and then as you work on the project you have to keep excluding more things or unintentionally include them in the WebAssembly build. In the above example, all the .razor components end up in the wasm build pointlessly, increasing its size just because it's painful to keep excluding things.

Benefits of approach C ("two projects")

  • No need for multitargeting
  • No need for any new naming or folder conventions that include/exclude things
  • No need for new rules about annotating project/package references with ClientOnly/ServerOnly/ServerAndClient/etc
  • Potentially an easier approach for people adding WebAssembly support to an existing ASP.NET Core server app (the .Client project could be a template in itself)

Its main drawback is that it gives up the multitargeting-based way to call server-only code from components that are shared with WebAssembly. For example, with approaches A and B, you could use #if SERVER inside a component to have a block of code that calls AppDbContext directly, with an #else block that runs on WebAssembly and maybe does an HttpClient call. That wouldn't work with option C because the .Client project couldn't reference types that live in the server project (there's no project reference in that direction). It means developers have to go back to traditional interfaces+DI, e.g. IMyRepository with different implementations for server and WebAssembly, since they can't just use #if SERVER etc.

In terms of whether the two-project system is harder to understand for total .NET newcomers, I honestly don't know. An extra project is an extra concept, however multitargeting and filename conventions are probably even thornier extra concepts still.

Proposal

As you can probably tell, between options A and B I'm currently leaning towards option A, however I'm still undecided on a preference between A and C. In the long term, having a single project will probably be an essential element of #46400, which may be a major feature for Blazor in .NET 9. So I suspect that's a likely direction eventually, however it doesn't mean that developers necessarily benefit in .NET 8 - there's an argument for keeping a simpler project system in .NET 8 and giving the single-project-multitargeting-conventions system more bake time. But perhaps I'm missing something about why we need to do a particular thing now in .NET 8.

If you have any feedback on what is wrong or missing from this analysis, please comment below!

cc @dotnet/aspnet-blazor-eng

@SteveSandersonMS SteveSandersonMS added area-blazor Includes: Blazor, Razor Components design-proposal This issue represents a design proposal for a different issue, linked in the description feature-full-stack-web-ui Full stack web UI with Blazor labels Jun 28, 2023
@SteveSandersonMS
Copy link
Member Author

BTW the way I think option A could best be implemented would be declaring an MSBuild property BrowserCompilationRoot with default value ClientShared. This actually means we support both A and B, since B is just a special case where BrowserCompilationRoot is set to "empty" or . (and of course then people can just use a different folder name if they want).

@SteveSandersonMS
Copy link
Member Author

SteveSandersonMS commented Jun 28, 2023

Project and Package references (for options A and B only)

There's also the question of references to other projects/packages. The same concern applies: for the WebAssembly build, we really don't want to include anything unnecessary by mistake, because (1) it can bloat the app size, and (2) it could lead to accidental disclosure of sensitive assets.

I propose we introduce a rule: For projects that multitarget net8.0 and net8.0-browser, all project/package references need to be annotated with ClientOnly, ServerOnly, or ServerAndClient, otherwise we log a warning.

For example, this is what would be in the .csproj by default:

	<ItemGroup>
		<PackageReference ClientOnly="true" Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.0-preview.7.23324.1" />
		<PackageReference ServerOnly="true" Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.0-preview.7.23324.1" />
	</ItemGroup>

... and if you add a new reference via tooling, then by default it won't have any such annotation, and so by default you'll see a warning like this:

image

The point of it being a warning is not to break unrelated tutorials (e.g., for EF, gRPC, auth, etc, especially for newcomers who might not yet know how to edit a csproj) but make clear that we expect developers to make a conscious choice about it at some point.

Update: None of this applies to option C. This is only a consideration for options A and B.

@mkArtakMSFT mkArtakMSFT added this to the 8.0-preview7 milestone Jun 28, 2023
@javiercn
Copy link
Member

@SteveSandersonMS Instead of having to annotate each and every package/reference, could we have some sort of more general rule about what references are allowed by default and what references are not allowed?

Something akin to what happens https://github.com/dotnet/sdk/blob/main/src/StaticWebAssetsSdk/Sdk/Sdk.StaticWebAssets.StaticAssets.ProjectSystem.props#L30

So that we can say something like

<PropertyGroup>
  <DefaultExcludedReferences>MyCompany.*</DefaultExcludedReferences>
</PropertyGroup>

For the vast majority of cases, if you even care about preventing a .dll from being sent down to the browser, it's likely going to be a dll from your org. If we can avoid you having to annotate Newtonsoft.Json, that would be great.

@johndhunter
Copy link

johndhunter commented Jun 28, 2023

@SteveSandersonMS - you may already have got the solution to "I spent ages coming up with that name, as almost everything else fails to communicate the idea that you're making stuff available to both client and server", - "Unified" - the other options being (dare I say it) "Common" or "Combined"

@SteveSandersonMS
Copy link
Member Author

@johndhunter Those are definitely reasonable suggestions too. "Common" was in fact an earlier iteration I used - the part about I thought may be worth changing is being more explicit (as in, what is in common with what, since the project maybe used for many things besides just Blazor so there are a lot of different concepts - MVC, Razor Pages, gRPC, auth, etc.).

@SteveSandersonMS
Copy link
Member Author

For the vast majority of cases, if you even care about preventing a .dll from being sent down to the browser, it's likely going to be a dll from your org.

I'm not sure. While that's probably true for the "unintended disclosure" concern, it's not true for the "bloating the wasm payload" concern. The particular example you chose of Newtonsoft.Json is a great one to think about because that's precisely the top example of something you really don't want in your wasm bundle for no reason (it's very big) but @danroth27 said he has seen people include it by accident in existing Blazor WebAssembly apps.

@jonlipsky
Copy link

jonlipsky commented Jun 28, 2023

@SteveSandersonMS It would be great if it would also give warnings about transitive package dependencies. I know in my own usage, that's how a few large assemblies ended up in my Blazor WASM applications.

@mrpmorris
Copy link

What is the benefit of having all of these in a single project as opposed to what we have now?

I like the two separate projects because you can't accidentally send server classes to the browser.

Having them as separate projects is 100% clear and immediately obvious, whereas both of these approaches seem like hidden behaviour for which you have to discover the rules.

@MackinnonBuck
Copy link
Member

MackinnonBuck commented Jun 28, 2023

@SteveSandersonMS I really like the ideas here! I definitely agree that Option A would be a better choice, purely because it requires intentionally making things available to the client.


Would the *.Client.* file name convention only apply within the ClientShared folder? I ask because, without reading any descriptions, the name ClientShared to me implied "everything in this folder is shared with the client". But I guess technically, what it means is "everything in this folder is shared with the client, unless a file uses the .Client file name convention, in which case it's actually not shared and only available on the client." That wasn't immediately intuitive to me because I wouldn't consider "available to the client only" to be a subcase of "shared between the client and the server."

Maybe an alternative to the .Client convention is another top-level ClientOnly folder? That way, there's a clear separation between what's "server only", "client only", and "shared between the server and client". Although, I can think of some downsides to that approach as well (there are two Program.cs files, there isn't just a single location for all client-available components, etc.).


The package reference idea seems good. My only concern there is that the mainstream scenario requires editing the project file manually to address warnings. Ideally, it would be nice to reach a point where the customer makes the decision at the time of adding the package (via the VS UI or a CLI option) rather than after the package was added.


@mrpmorris also brings up an interesting point. It does seem to me that a lot of the effort put into this design is trying to emulate features that would otherwise be achieved automatically by using multiple projects. Would separate .Client, .Server, and .Shared projects actually make things simpler because it's less for the customer to learn and less complexity for the framework to manage?

Perhaps the single project approach works well with MAUI because in that world, general application functionality is almost always applicable for each platform. And in the cases where it's not, there isn't as big a need to exclude it from a specific platform for bundle size and security reasons. Whereas, with Blazor, deciding where the code should run will end up being the first step for almost everything that gets added, whether it be Razor components, package references, etc.

And I could imagine the easiest way for the customer to make that decision would be to pick which project the thing they're adding is most relevant to, add it, and be done. Eliminating the "which project?" question actually just adds two more questions (which folder does this go in and what file name convention needs to be used?), and in the case of adding a package reference, adds a question in the form of a warning in the error list.

Not trying to say we shouldn't support the single project approach - I'm just playing the devil's advocate because I think these are interesting things to consider 🙂

@codeus-de
Copy link

My vote for option A - I think separation by folders is much more visible compared to searching parts of each file name for .Server or .Client in it.
One Idea for the folder name, that instantly came to my mind as I read your description "...you're making stuff available to both client and server...": "ClientAndServer". And the next idea is to only have one more folder on the root level that is called "ServerOnly"

@SteveSandersonMS
Copy link
Member Author

Thanks for the responses.

Following some interesting thoughts from @mrpmorris and @MackinnonBuck I've updated the description to include an "option C" with two projects. Thanks both of you for that - it's definitely helpful to question the assumptions we've made so far, whether or not that results in a radically different outcome. See updated description above for pros/cons of option C.

Please keep the feedback coming.

@claudiobernasconi
Copy link

I understand the intentions behind a one-project solution. However, one of the .NET "rules" I internalized is that a project has a build target, and whatever is in that project will be in the output DLL.

Of course, .NET has developed over time, and I assume (I'm not familiar with .NET MAUI) that there are already exceptions.

However, in the traditional ASP.NET Core world, where I am most familiar (and probably a majority of .NET developers), I believe option A and option B introduce concepts that are hard to explain and could be frustrating for beginners to learn.

I lean towards option C here.

One of the drawbacks is that you cannot use the same component server-side and client-side. I'm trying to figure out how often developers want to do that. Also, I try to stay away from using #if etc. to keep testing simpler. I might be biased, but I grew up with traditional .NET, where using interfaces and DI was one of the core principles, and I don't think it's bad using it for edge cases like this.

What I assume could be a common pattern is that we implement "presenter" components that receive data via parameters. We can then have different "functional components" (higher-order components) that load data and provide it to those "presenter" components. This way, we can use the same presenter components, client-side and server-side. With option C, that means having a shared class library including those presenter components. Maybe not very convenient, but explicit, and simple.

Generally speaking: When it comes to API calls and/or what goes into what DLL, I prefer a simple, straightforward concept with a little more typing/manual work over a complex solution where you might accidentally leak secrets or bloat your WASM assembly. Also, it's not a great experience when you do it wrong, and as a result, you might believe that WASM is slow, etc. when it just lets you make mistakes easily.

I took some time to come up with a fourth option, but I have nothing so far. If there is not a lot of pressure to have the single-project solution with .NET 8, I'd advocate learning more about how people use Blazor with .NET 8 and all its new capabilities. And use those learnings to create a single-project template with .NET 9.

@jburman
Copy link

jburman commented Jun 29, 2023

I immediately gravitate towards approach C for its apparent simplicity and since it cleanly reflects the deployment boundary, but I can also see the benefits of approach A for small projects. I dislike approach B as it looks like it would be very challenging to upgrade a larger existing project, as well as to maintain it on an ongoing basis.

In thinking through one of my own projects, this is how I am imagining it would work…

Today I have an app that consists of server side (razor pages) for logging in, and then it boots up a Blazor server app that runs a bunch of admin pages as well as a product catalog (or catalogue for my friends across the pond 😀). When I upgrade to .NET 8, what I would like to do is the following:

  • Run the Blazor portion of the app as SSR (so, the top-level Layout and menus)
  • Mark each of the individual admin pages (that need it) to be signalr based so that they will run as today.
  • Run the catalog page as Wasm

The first two items hopefully don’t require any changes to the components or the server-side DI.
The Wasm based catalog would require the following changes:

  • A web API needs to be surfaced for the catalog and an API client created.
  • For approach A: I would add a ClientShared folder and place my catalog components there, along with the API client and a Program.Client.cs.
  • Ignoring approach B here as it would require tons of changes in the overall app.
  • For approach C: I would add a new Client project (targeted at net8.0-browser? Not sure what SDK it would use either) and place my catalog components, along with the API client and Program.Client.cs in there.

In reality, it’s a bit more complicated because I re-use the catalog in a hybrid app as well, so all of the catalog components are in a separate RCL, along with some extra CSS and JS. Access to the catalog is abstracted to a CatalogItemProvider. But that doesn’t change too much from above. I would be swapping in a CatalogItemProvider that uses the API for example.

Finally, while not something that I am looking to do, would approach C also make it possible to reference more than one client project from the server project and then use them on different pages?

@DrLeh
Copy link

DrLeh commented Jun 29, 2023

I like C because it's what I'm doing on all of my applications already- separating Blazor WASM from the web API that's hosting it. Makes it perfectly clear without special conventions what files are used where. This project reference to the client app makes it clear that the shared logic is one-directional, that nothing in the server that isn't needed for the web app will ever be included in the UI because it's not in that project.

@robertmclaws
Copy link

Honestly the biggest problem with the old project layout was that you folks threw everything into subfolders instead of just keeping them at the root of the project.

Since just fixing that is not one of the listed options, I'm going to suggest Option C, but it needs a different naming convention, and a suggestion for clarity moving forward.

"Client" should ONLY be used to reference server API consumption libraries. Because, what if you put Web APIs in your Blazor back-end, that people from outside Blazor also need to call? (I don't recommend it, but people certainly do it.)

Then you will have "SomeWebApp.Client" and SomeWebApp.ApiClient", or some permutation of that... which does not bode well for design.

If you go with Option C, you should call it "BlazorWeb", and "BlazorWeb.FrontEnd". Because the app fka "Client" is unequivocally the "front end" of the application.

Under that name, there will be no confusing what the purpose is. No misunderstanding if it has API implementation classes to consume. Clean, simple, unmistakable.

My $0.02. Keep up the great work!

@Markz878
Copy link

Markz878 commented Jun 30, 2023

I currently also lean towards option C, because it makes all the magic very obvious. That template also needs a Shared project, because how else would the Client project get the DTO classes? So that makes it basically the WASM hosted project, which I do like.

The biggest issues I have with the WASM hosted model currently are that the default template doesn't use prerendering which makes the initial first paint timing too long. With Blazor getting SSR that should be the default now, and we just replace all .cshtml files with .razor files. Also, the data persistance model required by prerendering has too much seremony, I wish we could get a [Persist] attribute like Javier showed in one of the stand-ups, or maybe it could just work somehow automagically? And antiforgery tokens in the default template for form posts.

I use Damien Bowden's template that uses a cookie based BFF pattern, where client side authentication/authorization is handled by an "GetUserAuthenticationState" API call. This makes it possible to get prerendering to work with authentication (I don't remember why but I also had to ditch the CascadingParameter way of passing auth state and just cache the auth state in a singleton). Cookies can also be used to load for example saved form data in the prerendering phase. This is mandatory IMO because we don't want to wait for the WASM to load to access local storage, since it takes too long.

If all of this could be crammed into option A where Blazor Server and WASM could be used interchangeably, that would also be nice, but that would require some serious innovation, like for example using some attribute with source generators to turn data loading component functions into a http/short lived signalR call if run on WASM side (just spitballing here, how expensive is signalR connection creation compared to http call)? Then the project might work like you showed in your initial demo. Using #if directives might also work, but sounds like a testing nightmare..? Maybe borrow the Sveltekit concept that the default way to pass data from client to server is though form posts. Then if the WASM hasn't yet loaded, it would work as an MPA. But as has been said, this would require some serious magic under the hood that is not very obvious, and would probably run into some strange edge cases.

So all in all, I'm happy with option C and hope we get the improvements I mentioned here. I don't really care about the Blazor Server style project because it's relatively heavy on the server and the SPA stops working/resets when the server restarts or when there are network issues. Of course if the project could start as Blazor Server style and immediately transition seamlessly under the hood in to WASM mode, that would be ideal. I was hoping that the Auto-mode you mentioned would do this, but this has the same issues to be solved as I mentioned in the previous paragraph.

@SteveSandersonMS
Copy link
Member Author

That template also needs a Shared project, because how else would the Client project get the DTO classes?

They can go in the client project, as classes there are available everywhere. If people want to add a third "shared" project they are always free to do so, but there's no technical requirement for one.

@ThatRendle
Copy link
Contributor

ThatRendle commented Jun 30, 2023

Very very very much in the "two projects" camp here. I know we're kinda sorta competing with Node+SSR[React/Vue/SvelteKit/Angular] and those are all one "project", but they're horrible projects with mad quantities of config files and npm modules. Differentiating compilation based on folders or naming conventions within a single project has a whiff of magic strings about it, and having to deal with #if pragma in application code introduces additional complexity and confusion with IDEs needing to switch between build targets for code analysis, IntelliSense, etc.

@davidroth
Copy link

I am also voting for Option C. It solves many issues of A in a clear and ideomatic dotnet way. It does not introduce strange new and confusing conventions. I also dont see it as a drawback that C doesnt allow "#If Server" because that can easily grow into a giant hack. The IRepository aproach is a reasonable and clean alternative IMO.

@mrpmorris
Copy link

@SteveSandersonMS
I'm still not sure of the real-world application here.

People who develop Server apps tend to like using DbContext and editing Entities directly, whereas people who write WASM apps obviously cannot do that so use API contracts and serve those from the server instead.

Using #if Server etc would effectively mean writing two different apps not only in the same project, but also in the .cs files themselves, which isn't really good for maintenance.

If the developer wanted to have their app switchable between Server and WASM, then the better approach would be for them to always have their UI work with DTOs, and have the services that provide the data registered differently on the WASM project compared to the Server project.

The WASM app would call an API to get the DTO, whereas a Server app would create the DTO from the DBContext.

Obviously this is a more complicated approach than directly editing Entities, but I think it is also much more manageable than mixing the code into a single project and marking which classes go to the Sever vs WASM and using #if directives for the cases where the class goes to both but behaves differently.

Ultimately, people would still be able to do WASM how they do it now, and do Server how they do it now. If they want code that is switchable between the two then they have the option to add the complexity of different behaviours themselves in whatever way they prefer (e.g. registering different services).

I find option C far less prescriptive in its approach. It allows people to come up with their own solutions (which coders love), and separates client/server code in a way that everyone is already familiar with because it's the way we've always done it.

@marinasundstrom
Copy link

I would vote for Option C.

I don't think that the "single project" is a road where we should go down. The MAUI model doesn't work for Blazor since there will be two apps that have different entrypoint, components, logic etc.

Separating out components based on render modes is not an easy task to do. The experience will be terrible. And I'd rather not that we overcomplicate it by hiding the implementation details.

Anyway, you will just import WebAssembly components into the base app - not the other way around. Keeping the render modes in separate projects is the best option.

And it will be easier for the ones building the tools to create a better experience for developers.

@alexei-lazari
Copy link

I would vote for Option C.
It is the simplest path in my opinion. Already using this and is relative simple to understand and configure, inclusive references.

@ADefWebserver
Copy link

I vote for Option C. The reason is I mostly create Blazor Server projects and only use Blazor Web Assembly for PWA apps. The devs on my team have no problems understanding the current single project Blazor Server structure. It only takes 2 minutes to explain the multi-project Web Assembly structure. In my opinion Option C already works.

@Pinox
Copy link

Pinox commented Jun 30, 2023

@SteveSandersonMS

Personally I get the hat that you put on for Blazor & ASP.NET but what about if you make the project structure similar to what is used in MAUI. Therefor it's nothing new and will be awesome for everyone that is not just interested in Blazor or ASP.NET.

image

In the example above I can use Blazor server for web first development (fast/hot reload advantage) but can now also extend my Blazor app to all platforms still within the same project structure. (with MAUI Blazor.)

Less confusion in my opinion and super awesome for any cross platform Blazor.

So simplistically I can reduce this then to only the following if you are not interested in the MAUI part. You now have a folder and not just a file to throw in all the parts that should be only applicable to that platform. So in essence any multi-targeting project should follow the MAUI implementation for consistency in the .NET ecosystem.

image

Running 6 platforms in one project structure will be my dream ;))

If I had to pick one of your options it would be option C. I pick option C because that would be the easiest way to integrate into a cross platform scenario as expressed above with my example.

Multi-targeting Blazor would be super awesome for sure !!

@danroth27
Copy link
Member

would approach C also make it possible to reference more than one client project from the server project and then use them on different pages?

@SteveSandersonMS Thoughts on this?

@Romfos
Copy link

Romfos commented Jun 30, 2023

From my point variant #3 is better:

  1. I have clear understanding - what code is working on backend, what on client
  2. I have clear understanding - what are my dependencies on both sides
  3. Better from security poitn of view - I have guaranties that no database credentials, e.t.c will be pass to client side. It just harder to use in a wrong way

Variants #1-2 works only for helloworlds, but for real world app are not so good.
At the same time I agree that on the demo on dotnetconf it will be look "nicer" =)

on more note:

Benefits of approach C ("two projects")
No need for multitargeting

if i right understand it still will be working on both sides and second project is still multitargted for net8.0 and net8-browser
otherwise it cannot be referenced by server project

Even if you decided to use option make sense to make it consistent with MAUI:

/Platform/Server/Program.cs 
/Platform/Browser/Program.cs
/Program.cs
/Components
/Views

paltfrom specific code in "/Platform/"
all other code - shared
Maybe make sense to make it default for any project\app type for multitargeting and do not reinvent the wheel every time?

@KristofferStrube
Copy link
Contributor

I'm interested in how this will play together with using Razor Class Libraries targeting both Blazor Server and Wasm and how we can minimize the work needed for a library consumer when adding a third-party library to their project. What I most often see is the need to add some service that is injected into each individual page or component through the DI container to add some extra functionality. My impression is that a lot of vendors of libraries already have functionality in place that makes it possible to call the same function to add the needed services and potentially make configurations automatically depending on whether the project is Blazor Server or Wasm.

So having two separate Program.*.cs files that would both need to add multiple services in the exact same way seems like an extra step to have to explain to consumers of libraries. My guess is that most will probably just end up adding some extension method to the project (or just the Client project if solution C is picked) which adds the shared services and call that in both Program.*.cs files but it seems a bit clunky having some extension method called .AddCommonServices(this IServiceCollection serviceCollection) as that doesn't explain what is added or why these are grouped together apart from being needed both places. AddUIServices might be a better name for this method.

I'm not saying that we need to have a good solution for this just that it seems a bit cumbersome. I haven't used MAUI so it might be interesting to see how they manage to have services set up while still targeting different platforms with different needs/capabilities without extra extension methods or code duplication.

@SteveSandersonMS
Copy link
Member Author

@mayoatte How would razor class libraries work in these scenarios?
@KristofferStrube I'm interested in how this will play together with using Razor Class Libraries targeting both Blazor Server and Wasm and how we can minimize the work needed for a library consumer when adding a third-party library to their project.

If there was a single-project option (A or B) then it would be multitargeted over net8.0 and net8.0-browser. Class library authors could either target net8.0 and then write code that works in both environments, or they could also multitarget the class library over net8.0 and net8.0-browser and then use conditional compilation to put in different implementations or functionalities for the two platforms.

@SteveSandersonMS
Copy link
Member Author

Thanks everyone for all the feedback here! Clearly there's a strong community consensus, which is that most people prefer the two-project approach (option C). We're keen to listen, and so the two-project approach is how we'll structure the new default Blazor Web template. We recognize its advantages in terms of being more familiar and standard, with less need for new compilation rules or conventions.

A couple of points just to ensure everything is fully communicated:

  1. This means the new unified Blazor architecture will look slightly different to what we showed in demos earlier this year. For example, in a video about the prototype, we had a single project and toggled components between SSR, Server, and WebAssembly modes just by setting a @rendermode, with no other changes needed. In the two-project system, making a component run on WebAssembly will also involve moving it to the WebAssembly project (the .Client project in the diagram above) if it's not already there. Hopefully that's not a big problem for anyone.

  2. We are still open to experimenting with single-project approaches in the future. For example, we might make an unofficial, experimental template package to let people try this out if they want (as part of other experiments). But to be clear, we hear the overwhelming consensus is that people want a multiproject approach and so that is the default and in fact will be the only WebAssembly-enabled option for .NET 8.

I hope that sounds right and makes sense. This approach (C) will now appear in an upcoming preview release, perhaps preview 7 (note: not preview 6, as that's already locked down).

@Pinox
Copy link

Pinox commented Jul 4, 2023

Thanks Steve. I think you might have had a different result or at least more balanced vote if you included a pure "MAUI based" multi-targeting approach but that was not on the table here. I for one would have preferred that over options C (although I voted option C) as Diagram A + B is much more confusing than a "MAUI multi-targeting" approach. So the result you got here is more reflective of the suggested designs.

@SteveSandersonMS
Copy link
Member Author

@Pinox The big issue with MAUI-style conventions is that it would not be compatible with existing ASP.NET Core applications, as it would force the developer to move all server-specific code into a Platforms/Server subfolder. I'm sure it's technically possible for people to do that, but most server app developers would not appreciate this kind of disruption, and it would mean all existing tutorials and docs (for unrelated server features like gRPC or auth) would not match up with the project structure by default. However I appreciate people will have a mix of different priorities, and some may be OK with restructuring the server code to fit in with such a structure.

@Pinox
Copy link

Pinox commented Jul 4, 2023

thanks @SteveSandersonMS. Yea fully get that and I understand you have to put the ASP.NET hat on ;)) Perhaps you can include something like this in your unofficial version. I for one would luv something like this as I stand with one leg in the cross platform world and recently decided to go all in on Blazor even cross platform and I luv it.

@sbwalker
Copy link

sbwalker commented Jul 4, 2023

I am struggling a bit with the A, B, C options proposed, as although they seem to align with the stated goals (each with pros/cons), I am assuming there are additional options which could also satisfy the goals. In parfticular I am wondering why it is necessary to make such a significant deviation from the structure which was introduced in the earliest versions of Blazor and is probably the most widely used approach in use today.

I am referring to the client/server approach where there is a distinct Client, Server, and Shared project (ie. the structure produced when you use the Blazor WebAssembly template).

The Oqtane framework (https://www.oqtane.org) has successfully used this approach to allow developers to build applications which support Blazor Server and Blazor WebAssembly (and Blazor Hybrid) within the same installation (ie. it eliminates the need for developers to make a choice about hosting models - it is purely a run-time consideration). Applications are packaged as RCLs which contain a single set of binaries and static assets - which can be run on any Blazor hosting model. Oqtane is able to accomplish this by using a convention where applications must be written using a client/server approach so that they can run in all environments.

image

In regards to the stated goals, this approach provides the following benefits:

  • Make it obvious what ends up in which compilation - there is only a single compilation - one set of DLLs for developers to manage and maintain. The Client is targeted at the Client, the Server at the Server, and the Shared is targeted at both Client and Server (and could be called ClientShared to make it 100% clear but at this point developers are familiar with this concept already).

  • Stay compatible with existing ASP.NET Core conventions - this is the existing convention so no issue here

  • Maximize clarity - Client = Client, Server = Server is very clear and already familiar to Blazor developers

  • Work without tying people to particular project structures - this is a bit idealistic - at the end of the day there are certain approaches which are going to work and others which will not. Developers simply want a strting point which ensures success and reduces guessing.

So I am curious why this approach is not represented amongst the A, B, C options. Are there some fundamental differences in Blazor Unified which prevent this approach from working? Before introducing a major conceptual shift it would be helpful to understand why the current approach is inadequate. This will also help determine the migration path for existing applications which are using current conventions but want to take advantage of the new .NET 8 capabilities.

@SteveSandersonMS
Copy link
Member Author

@sbwalker Option C is the same as the existing Server/Client/Shared setup, except "Shared" is purely optional as it is not technically required for anything. People can add as many extra "shared" class libraries as they want but it's not required by the system, as you can also make code shared by putting it in the Client project. It's not a big change from the existing setup :)

@sbwalker
Copy link

sbwalker commented Jul 4, 2023

@SteveSandersonMS Well then my vote is definitely for Option C as I think the familiarity and migration path are significant benefits to the Blazor community.

I also agree that the Shared project is purely optional. In the traditional Blazor WebAssembly template it includes shared Models which is useful for transferring data between the Client and Server... but is not always required. And you are correct that the models could be included in the Client project to achieve the same result.

@smitranic
Copy link

smitranic commented Jul 4, 2023

+1 for Option C

And I will add another angle to consider re: extending Option C to explicitly support Server / Client / Shared structure.

For deployment there are really two targets: Server and Client. And the question is how to organize the code so that it ends up in either or both of those deployment targets.

Option C as currently specified has structure to organize the code into the following "buckets":

  • Server (with Client included) which is really "Everything" (Server + Client + Shared) and it runs on the server.
  • Client which is really Client + Shared.

Those two code-organization options don't really align fully with the deployment targets. Client-Only option is not explicitly represented, and I assume it is fully supported in Option C just like it is in Option A and Option B. But now tagging code to be Client-Only is very different and asymmetrical from the other options.

I also assume that since we are talking about multi-targeting applications, bulk of the Blazor application code (i.e. not domain logic or data access code) would be targeting both Client and Server. That means that bulk of the code would be in the Client project and Server project will be relatively light as far as Blazor code goes. This goes for migrating existing projects too - to migrate a project to multi-target Server and Client you really have to move bulk of the Blazor code to the Client project. I think this kind of disruption was one of the arguments against the Shared project (or the MAUI-style structure).

My 2 cents is that adding a Shared project to Option C and changing the Client project so that it is explicitly not included in the Server would provide for a more intuitive structure with least surprises.

@SteveSandersonMS
Copy link
Member Author

SteveSandersonMS commented Jul 4, 2023

@smitranic This design is only about the new Blazor unified architecture style, which is innately server-hosted, so client-only deployment isn't an applicable concept. We continue to support standalone WebAssembly apps (this remains very important) but it won't use this template - it will just be a standalone project not involving any server.

@msauper
Copy link

msauper commented Jul 4, 2023

Aside from the project organization issues, to address the code confidentiality issue, have you consider some class or assembly attribute that explicitly flags that an assembly should not be send to the client. This would presumably raise some error in the event that the projects were not constructed correctly.

@smitranic
Copy link

smitranic commented Jul 4, 2023

@SteveSandersonMS I was referring to the *.Client.* file name convention in Option A and Option B as a way to explicitly target only the Client and exclude code from the Server.

Now looking back over the comments, I don't see it explicitly mentioned except for Program.Client.cs.

Is it meant as a generic convention for any file, or is it specifically meant only for Program.Client.cs in Option A and Option B?

If it is meant as a generic convention that is supported in Option C, then using that mechanism to target only the Client (i.e. excluding that code from the Server) still feels quite "different" in Option C compared to the project-based approach that Option C is using. Client-only code would have to live under something like MyBlazorWeb.Client\SomeClientOnlyFile.Client.cs to be truly Client-Only (i.e. only compiled into WASM).

@sbwalker
Copy link

sbwalker commented Jul 4, 2023

In regards to Option C and the comment from the original post...

"Its main drawback is that it gives up the multitargeting-based way to call server-only code from components that are shared with WebAssembly. For example, with approaches A and B, you could use #if SERVER inside a component to have a block of code that calls AppDbContext directly, with an #else block that runs on WebAssembly and maybe does an HttpClient call."...

I would prefer to avoid conditional compilation at all costs. The extra complexity and likelihood of misuse is not worth it. And it seems to be contrary to the Clarity goal as it embeds magical hardcoded behaviors into your code which are difficult to locate and test.

In Blazor 1.x I actually tried to use conditional compilation in Oqtane to support Blazor Server and Blazor WebAssembly in the same code base - and it was a total nightmare. Eventually finding an architectural pattern which supported both hosting models without conditional compilation was a huge win (thank you Carl Franklin). I would choose a cleaner abstraction over conditional compilation every time.

The use case you included above related to calling the database directly on Server and using HttpClient on WebAssembly is already possible by including a service layer with the same contract in both client and server (a clean architectural approach instead of a conditional compilation approach)

@robertmclaws
Copy link

@smitranic This design is only about the new Blazor unified architecture style, which is innately server-hosted, so client-only deployment isn't an applicable concept. We continue to support standalone WebAssembly apps (this remains very important) but it won't use this template - it will just be a standalone project not involving any server.

What about Blazor WebAssembly apps hosted from ASP.NET Core websites?

@SteveSandersonMS
Copy link
Member Author

What about Blazor WebAssembly apps hosted from ASP.NET Core websites?

That's not a client-only deployment.

@patrickjahr
Copy link

I agree with option C.
Since the handling of services between WebAssembly and Server is significantly different, the components are the same, but the dependencies and data access usually differ.
Therefore it makes sense that the WebAssembly part is a separate project and separate from the server part.

@SteveSandersonMS

  • Can WebAssmbly and Server components be used on one razor page?

@SteveSandersonMS
Copy link
Member Author

Can WebAssmbly and Server components be used on one razor page?

Yes, as long as neither is nested inside the other.

@sharpist
Copy link

sharpist commented Jul 6, 2023

I would choose option C. I agree with @dmitry-pavlov and @patrickjahr. 👍
The structure with separate projects looks much better and cleaner.

@Aquaritek
Copy link

Aquaritek commented Jul 7, 2023

I think that option C is the best option here. Overall, I think what's being built here is fairly novel - meaning that some conventions will get lost as new ones are imagined. I wouldn't be afraid of this because in the end its progress.

I do think more conversation needs to be had on service layer though. How would we actually code a service implementation for execution both Direct and over Http? Also, would it be beneficial to include this in the initial example project? These are probably questions for other threads because it's less about literal project structure at that point.

@sbwalker
Copy link

sbwalker commented Jul 7, 2023

@Aquaritek

The following architectural pattern accomplishes your goal:

Client.Project
  /Components
    MyComponent.razor
  /Services
    MyService.cs
  Startup.cs
Server.Project
  /Controllers
    MyController.cs
  /Repositories
    MyRespository.cs
  /Services
    MyService.cs
  Startup.cs
Shared.Project
  /Models
    MyModel.cs
  /Interfaces
    IMyService.cs

In this approach there is a service interface which is defined in the Shared project (this could actually be in the Client as per Steve's comments above). This is the contract which a service needs to implement. In the Client project there is an implementation of the service which leverages HttpClient to call the Controller API endpoints. However in the Server project there is also an implementation of the same service which calls the repository directly (ie. no HttpClient as this service runs on the server). The Client project components would utilize the service interface and dependency injection to obtain a reference to the appropriate service at runtime. If you are running on Blazor Server then the Server project startup is executed and it would contain service registrations for the Server services. The components would run on the server so they would use the Server services. If you are running on Blazor WebAssembly then the Client project startup is executed and it would contain service registrations for the Client services. The components would run in the client browser so they would use the Client services.

@SteveSandersonMS SteveSandersonMS modified the milestones: 8.0-rc2, Discussions Jul 7, 2023
@SteveSandersonMS SteveSandersonMS added the ✔️ Resolution: Answered Resolved because the question asked by the original author has been answered. label Jul 7, 2023
@ghost ghost added the Status: Resolved label Jul 7, 2023
@ghost
Copy link

ghost commented Jul 8, 2023

This issue has been resolved and has not had any activity for 1 day. It will be closed for housekeeping purposes.

See our Issue Management Policies for more information.

This issue was closed.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-blazor Includes: Blazor, Razor Components design-proposal This issue represents a design proposal for a different issue, linked in the description feature-blazor-single-project feature-full-stack-web-ui Full stack web UI with Blazor ✔️ Resolution: Answered Resolved because the question asked by the original author has been answered. Status: Resolved
Projects
None yet
Development

No branches or pull requests