dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.36k stars 9.99k forks source link

Blazor unified project design #49079

Closed SteveSandersonMS closed 1 year ago

SteveSandersonMS commented 1 year ago

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

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")

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")

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")

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 https://github.com/dotnet/aspnetcore/issues/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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

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:

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 commented 1 year ago

@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 commented 1 year ago

@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 commented 1 year ago

+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":

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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

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

That's not a client-only deployment.

patrickjahr commented 1 year ago

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

SteveSandersonMS commented 1 year ago

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

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

sharpist commented 1 year ago

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

Aquaritek commented 1 year ago

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 commented 1 year ago

@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.

ghost commented 1 year ago

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.