dotnet-architecture / eShopOnWeb

Sample ASP.NET Core 8.0 reference application, powered by Microsoft, demonstrating a layered application architecture with monolithic deployment model. Download the eBook PDF from docs folder.
https://docs.microsoft.com/dotnet/standard/modern-web-apps-azure-architecture/
MIT License
10.13k stars 5.49k forks source link

Clean Architecture with Identity #39

Closed J0nKn1ght closed 6 years ago

J0nKn1ght commented 7 years ago

Hi, We're starting a major new web project, and wanted to start developing in .Net Core, rather than the MVC5 that we're used to. I've read through some of the Architecture guide, and was interested in moving to using the Clean Architecture approach. I was hoping that this sample application would indicate how this could be used in an ASP.Net Core web application, but it seems to duck some of the issues that it's trying to promote. Specifically, we need to have a relationship between our business entities, and the application user, but this project doesn't implement this aspect. As a scenario that would crop up in an eShop, it's likely that a user of the site would need to see their order history. So, in EF, you might have an Order entity, which has a UserId property, and then a navigation property to the ApplicationUser. Similarly, the ApplicationUser might have a navigation property to a List of 'Order'. In the sample, ApplicationUser isn't in the Core project, but is only in the Infrastructure project, so how would you implement this? I realise that there is a solution to this, presumably using interfaces for the Identity classes in the Core project, but it would be useful to have this in the sample project, if it is intended to get developers up and running quickly. If I've missed anything obvious, let me know. Thanks!

trevor-hackett commented 7 years ago

Unless you really needed to have the navigation properties, you could keep the ApplicationUser separate and don't have navigation properties. You'd store the userId with the Orders (and anywhere else that you need) and if you had the userId already, you could just query the Orders where the userIds are equal.

J0nKn1ght commented 7 years ago

Ok, maybe my example was a bit simplistic. In real life, you would want to be able to do a query such as 'select all Orders where Order.OrderDate < x and Order.User.LastName.Contains(y)'. Not having navigation properties precludes this being a database query. I did try to take this approach, and keep ApplicationUser in the Infrastructure, and have a UserProfile model in Core, which shared the same ID and had the navigation properties to my business entities. However, the first thing that I wanted to do was create a user admin feature, where you could filter users by various properties, including the roles they were in. Not having navigation properties makes it impossible to run this query at the database level, and when the system can potentially grow to 10,000s of users, retrieving the data and querying in memory isn't really viable. This is still an issue with the latest version of Asp.Net Core Identity. I've ended up moving ApplicationUser back into Core, and adding the required references to Asp.Net Identity, etc., which obviously goes against the Clean Architecture principles.

DannyAllegrezza commented 6 years ago

@J0nKn1ght,

Any updates here? Are you continuing to keep the ApplicationUser in Core?

I'm curious because I was about to write a post asking the same question as you. I was curious if @ardalis could expand on how we might keep a clean architecture yet still have identity.

I'm wondering if the IdentityGuid in buyer class is a hint at how one might accomplish this?

    public class Buyer : BaseEntity, IAggregateRoot
    {
        public string IdentityGuid { get; private set; }

        private List<PaymentMethod> _paymentMethods = new List<PaymentMethod>();

        public IEnumerable<PaymentMethod> PaymentMethods => _paymentMethods.AsReadOnly();

        protected Buyer()
        {
        }

        public Buyer(string identity) : this()
        {
            IdentityGuid = !string.IsNullOrWhiteSpace(identity) ? identity : throw new ArgumentNullException(nameof(identity));
        }
    }

Unless I am missing something, it appears to be unused.

J0nKn1ght commented 6 years ago

@DannyAllegrezza No, I'm afraid that I haven't seen anything yet which suggests a solution to this problem. In fact it's got worse, in that I've also ended up adding a reference to Microsoft.AspNetCore.Mvc.Core in my Core project, so that I can use the ModelMetadataType attribute in order to override the default display names for the 'Email' and 'PhoneNumber' properties in the IdentityUser class. Incidentally, I've also changed all of the 'Identity' related definitions so that they are typed as Guid, rather than the default type of string. If anyone has any further suggestions of how to properly implement a clean architecture with the current .Net Core tool set (v2), I'd be interested to hear how to achieve it.

ardalis commented 6 years ago

@DannyAllegrezza The Buyer entity's IdentityGuid is how the domain interacts with the identity system in this sample. It's intentionally separating identity concerns from domain concerns. You can see where this is used in the BasketWithItemsSpecification class, which will list the items in the Buyer's basket (or an anonymous user's basket). It's also used in the CustomerOrdersWithItemsSpecification.

ardalis commented 6 years ago

My recommendation if you're going to use the built-in identity features that rely on IdentityUser is to keep your definition of ApplicationUser in the Infrastructure class. In your domain, refer to this class's ID anywhere you need it, just as Buyer does. If you need to include additional details on the 'user' you can do so on a domain entity in ApplicationCore. For example, I could easily add FirstName and LastName to Buyer. It would then be pretty easy to create a specification that would return all orders from any buyer with a last name of 'smith', for example. As for specifying the default display names for Email and PhoneNumber, etc., these annotations should generally be applied to ViewModels, since you don't want to directly expose your domain model to your MVC Views (or APIs). Instead, create a BuyerViewModel class (for instance) which has only the properties the view will use, and add any additional metadata to this class.

J0nKn1ght commented 6 years ago

@ardalis Thanks. I did go down that route, but as I mentioned in my previous post, the first thing that I needed to do on the site I was developing was to add a User Admin function, which needed to be able to query users by Role, and that approach made it impossible to do that without the query running in memory. I guess the only option really is to write my own Identity provider, which I don't have time to do with this project. On the use of ViewModels, I know that this is the accepted approach, but I've never quite understood how to square this with the DRY principle. You are literally repeating every class that you need to use, and therefore increasing the maintenance burden. Also, this means that to some extent you have to duplicate the business logic in the ViewModel, by repeating the validation attributes that are required for the view to take advantage of client-side javascript validation. I can understand having a defined interface for the models when writing a public API, but for internal use within an MVC site, it just seems a bit redundant to me.

ardalis commented 6 years ago

Right, sorry I didn't address that. If you're following a DDD approach, you could probably consider your user/role administration as a separate bounded context from the rest of your application (I'm guessing - I don't know the details of your app's primary focus). It also seems likely to me that the user admin portion of the app, whether it's its own BC or not, is mainly CRUD and doesn't have a ton of complex business rules. In this case, there's not necessarily a lot to be gained by following a pure DDD approach and using many of its patterns. Just create something simple that exposes the data to the UI and be done with it, but ideally do this in such a way that it's separate (different BC) from your main app for which you are trying to follow more of a DDD approach.

Regarding ViewModels, the advice is similar. If it's a simple CRUD app to be used internally, you can probably avoid the extra effort. However, if it's going to be public-facing and/or it's going to have a complex domain model, ViewModels offer advantages. When you do use ViewModels, you should have different models based on individual views or requests to the server. That means they shouldn't simply map 1:1 to your domain entities, because at a minimum you probably don't need an ID property on a Create action/form, for instance. You may have other properties, like DateCreated, that shouldn't be exposed on your create/edit forms, either. And there are potential attack vectors exposed by using your data/domain model as your API/web model, as well. For example, imagine you expose your ApplicationUser class, which let's say has a List Roles property in addition to a FullName property, on a profile edit page. On the POST action for this page you accept an ApplicationUser as a parameter and let model binding do its work, and in this action use a DbContext to attach the model-bound entity and then save it. This works great. Except that if a malicious user doesn't just post their FullName, but also an array of Roles that includes "Admin" in it (and you have such a role), then they've just made themselves an admin. If instead you had a ProfileViewModel that only had a FullName property, this scenario would be impossible.

HTH.

J0nKn1ght commented 6 years ago

Thanks for your detailed reply, @ardalis. The app we're developing is a bespoke CMS, so the vast majority of it is focused on the admin site, with very little on the site itself. TBH, I'm wishing that we hadn't switched to .Net Core, as once you start to do any heavy development in it, the cracks begin to show (in terms of bugs and missing features). This isn't helped by the fact that VS2017 also seems to have numerous bugs in it. Although there have been a number of updates to VS2017, it now looks like there won't be an update to EF Core until late Q1 2018. As we have an initial release planned for Jan 2018, it means that we're having to drop out to SQL queries to work around some of the deficiencies in EF Core v2. I think that these factors, combined with trying to use Clean Architecture for the first time, was a recipe for frustration! Thanks for all of your advice, though.

trevor-hackett commented 6 years ago

The way identity and the entities are separated in this project is very good practise. In most systems in enterprise applications users and application data are completely separate in different databases if not completely different systems altogether. Normally you'd do your specific filtering logic by querying the user system/database and then take the results and join them against your application database. In most cases this is out of your control and something you have to live with. The way this project is structured, it forces you to think similarly.

Having said all that, do what makes sense for your specific application. Follow as many good practices and design patterns as possible. The key is to not over architect your application and make it more difficult than it needs to be. Don't be afraid to break rules where it makes sense for your application.

ardalis commented 6 years ago

You're welcome and sorry you're hitting frustrations. Let me know (via my ardalis.com site) if you think it would be valuable at all for me to have a look at your app and its setup. I may be able to help. I agree with you that 1.x of .NET Core and EF and the early VS2017 builds were a bit rough, but right now with 2.x of .NET Core and the latest VS2017 I think things are in solid shape and there are no blockers for my scenarios or those of my clients. I'd be curious to hear more about what blocking issues you're seeing. Cheers.

bgl3317 commented 6 years ago

This is an excerpt from the guide -

The user interface layer in an ASP.NET Core MVC application will be the entry point for the application and will be an ASP.NET Core MVC (or Razor Pages) project. This project should reference the Application Core project, and its types should interact with infrastructure strictly through interfaces defined in Application Core. No direct instantiation of (or static calls to) Infrastructure layer types should be permitted in the UI layer. Your application can avoid referencing the Infrastructure project, while still using its types at runtime.

Yet, in the sample app code, the web layer project references the infrastructure project and uses four separate infrastructure namespaces in the Startup file alone. This is really disappointing and seems to undermine the credibility of the architecture guidelines.

ardalis commented 6 years ago

It's that way (in the app) mainly for simplicity. In the excerpt above, not that it says "Your application can avoid" -> can, not must or should. There are benefits to avoiding the project reference, mainly in that it becomes difficult/impossible to have early-bound compile-time references to types in the Infrastructure project (you could still do it with reflection, but that requires a bit more effort). However, it's not trivial to configure this in ASP.NET Core with its default mechanism for DI and registering services like EF and Identity. The main reason is that these static helper methods mainly use generics like AddDbContext and for these to work you need to be able to reference T at compile time.

I have an article demonstrating how to do exactly what the guidance suggests in full .NET here: https://ardalis.com/avoid-referencing-infrastructure-in-visual-studio-solutions

I intend to write a similar article for ASP.NET Core but haven't made the time yet. I will be revising the sample and eBook for 2.1 in the very near future and I'll keep this feedback in mind and will try to at least show how to achieve this even if the sample keeps things simple and keeps the project reference.

Thanks for the feedback!

TungDao1102 commented 11 months ago

I found a solution on reddit that we can use fluent API to configure foreign keys without specifying navigation properties inside the class. For example:

public class Card { public int Id { get; set; set; } public string Name { get; set; set; } public string UserId { get; set; set; } // Other properties }

protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);

        builder.Entity<Card>()
            .HasOne<User>()
            .WithMany()
            .HasForeignKey(c => c.UserId);
    } 

What do you guys think about this approach?