-
Notifications
You must be signed in to change notification settings - Fork 166
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
Discuss Architecture Patterns #33
Comments
Just watched the video |
First of all, I greatly appreciate the effort put into this, it's great to see so many people finding time to participate and churn out ideas and code for just the good feels of it, kudos. I'm adding my 5 cents to the discussion, nothing below is meant to be personal, or addressed to any specific person (I didn't do a git blame to check commit authors). I'm also not advocating for any changes in framework or technology in this post (however will do so if prompted), this is purely my observation and is to be taken as constructive criticism with no ill intent, purely my attempt at improving things (some of it subjectively I'm sure) and starting a dialog as I'd like the project to succeed, long term. 1) project structure 2) API 2.2) noise (exceptions) to signal (actual logic) ratio is quite high, at a glance around 3/4 of the code is catching exceptions of different types, while doing similar (or in some cases exactly the same) things with them afterwards. Doing it like this is very costly from a maintenance and awareness point of view. Developers would typically copy paste these around. Code like this belongs into an action filter that will be applied globally for all of the endpoints, and the individual controller actions are thus freed up to only handle the actual logic 2.3) entities are being returned and accepted verbatim via the API including system attributes that should never be modified externally in an audited system. I made this example before, but with an interface like this it is possible to register a student with all A grades and graduated. Only a restricted set of properties should be modifiable via the API, the rest should be filled in by the system. @hassanhabib I know you mentioned that there will be some orchestration done on these to prevent a scenario like the above, but I see nothing in that direction in the project. 3) service layer 3.2) sooo many custom exception types. I count 8 custom exceptions for the Student entity only. Why is it necessary to distinguish between them? It is not clear to me which one to use when, e. g. NotFoundStudentException and NullStudentException, InvalidStudentInputException and StudentValidationException. Are there going to be another 8 exception types for the teacher as well? 3.3) services accept entities as parameters. I've made this observation in a PR, but will copy it here as well. 3.4) validation. The validation of a student is now 200+ lines of case and if statements. The cyclomatic complexity of this is enormous and writing all of that code is tedious, error prone and ultimately can be replaced by using a smart validation framework that allows you to apply validation rules declaratively. Even out of the box Data Annotations offer all of that functionality and could be applied straight on the entity if need be, or on a command object that would represent the intent of student registration/modification 3.5) storage brokers. What is the added value of these? All they do is abstract EF as the repository, which is both a leaky abstraction (e. g. necessity to do eager loading via Include, using Find where appropriate, etc) and an unnecessarily complex one. Purely for the sake of mocking, would it not suffice to expose a 3.6) auditing. How will this be performed? This ties a bit into the authentication system, and how the identity of the caller will flow through the system (if at all), but I'm also missing history tables attached to the entities (or something to approximate that functionality, e. g. event sourcing). 4) model 4.2) I like the use of EF migrations. I have 0 experience with using these, but it looks useful at a glance. 0) git usage -1) design |
@Driedas thank you for your dedication and your detailed identification of potential areas of growth in our architecture and technology.
The idea of building our components as brokers, services, controllers and models is that we are building vertical slices of flows for particular entities so eventually down the road it would be easier to split those off as microservices. We try to build a monolith with a microservices mindset. meaning that we are building our monolith intentionally to be broken down eventually once the application gets larger (and it will) - and when that happen offline and online processes will need to be their own microservices, we will make that call when we get to that point, but for now, we shouldn't start optimizing for a workload and a size that we are not sure of yet. A lot of people have asked me about the plan or big picture, or the high level goals, this is also very legit concern, however, as we are still simply just building the familiar entities of the system, I think that building these relationships and overviews are a bit too early. We know that we want to build a schooling system, and that the system has students, classes, courses and teachers and exams, great let's start building those with full CRUD operations then we will discuss where to go from there, I like to plan for the foreseeable future not a year down the road, because I can guarantee you right now, that whatever we plan now is going to almost fully change by the time we get to where we are going, I am not a big fan of waterfalls :-) Let's discuss this first, then we will move on to the other points, I will use your initial post as an overview of what this thread's discussion is all about, does that sound like a good plan? |
That is fair enough, I can see this potentially working, however I am skeptical that the core domain (students, teachers, guardians, courses, exams) is ever going to be split into multiple microservices in the future as that is the very core functionality of the system. Who knows, hard to predict the future, I agree.
I don't see the core domain as a good candidate for a CRUD style approach (you don't create or update or delete a student or teacher), that's not how future users visualize an education system so there would be misunderstandings around the mappings of concepts like a student update to mark a student as graduated. There are more interesting concepts (and a ubiquitous language to be found), but this is a discussion for a later point.
Awesome, I would also hope that at some point others would join in as well with their own suggestions and critique of my points as well. |
@Driedas I also expressed my concerns over some architectural details, but must admit, not in such details as you did.
Regarding the GIT structure and commits and "unit" testing:
Here the style how brokers are implemented are much "confusing". With non detached (and tracked) entities we do have much more comfort doing unit tests - especially with manually assigned guids (PK). Due to the ORM graph we can set up any situation in memory inside the entities and do the checks on those related objects. Would not say that I don't see some perv-like beauty in the no-tracking and repo-like (well, not repo as not aggregates) approach, but not sure this is the way EF should work (sure, this is the way how DAPPER or plain SQL works - worked, before ORM, so before 2004). Would also check the broker's get-by-id methods, as they use no-tracking queries. This is normal for querying read-only data, for some reporting and so (see: https://docs.microsoft.com/en-us/ef/core/querying/tracking). In our case it has some side effects:
Not sure if I posted it here, but these guys are more-over using similar approaches as I described above, nicely documented and explained: https://rules.ssw.com.au/rules-to-better-clean-architecture Now whether monolith or micro-services: clean code and proper architecture belongs to every code base (as junior developers do copy-paste, so if you start with eased rules, you end up with...). I know, from the compiler perspective it's equal what you got, but if you want to keep this project a living, extensible one, then readability and simplicity MATTERS! < @hassanhabib Ohh, btw: CRUD. I think there was a tool generating UI (for .NET) based on entities which required CRUD - and well, I can't remember, nor sure if ti still exists, but also a lot of banking stuff is processed via EXCEL :) |
@hidegh entities and their serialization issues is a good point I missed. Given a rich model (e. g. a Student has a list of Subjects he attends and the Subject has a collection of Students that attend it) a serializer can easily keep on going and going when lazy loading is enabled. This is one of the very few instances where I saw a production server going down in flames with an out of memory exception :-) |
@Driedas we create full CRUD operations on all entities not necessarily for business purposes, for instance, the delete or delete all operations, we have those so we can clean up what we create in our acceptance tests. Now, regarding creating or deleting students ..etc - there's a higher layer of services that I call processing these processing services take care of these very specific business terminology and flows (as long as they don't intersect with other entities, in which case they become orchesterators, coordinators or aggregators depends on how complex your business is)- watch this video to have a better view about these layers - the broker-neighboring services only run CRUD for entities, but any higher layer services will tell you something like: These higher layer services might combine two or more basic operations from a broker-neighboring service to create a business value, sometimes it just calls one operation and give a different name and so on. |
Why not just create a throwaway database as part of the execution of acceptance tests? On test startup you generate an empty database with seed data and every test resets the database to the initial state (in whatever way, restore from a backup of the database generated in the beginning, or something like Respawn https://github.com/jbogard/Respawn).
If those CRUD operations are there for testing and reporting, sooner or later somebody will call them as part of his use case (because why not, they are there after all) and introduce inconsistencies by modifying data that only the system is allowed to. I wouldn't trust an audit trail of a system that even exposes CRUD semantics on top of its core domain.
How and why? The current vertical slice already exposes CRUDs over the API. That's as high a layer as you can get. I think a sample would go a long way towards illustrating how that is supposed to look like. I saw the video before, and I agree on the public interface that should be exposed via the API (e. g. /students/1/deactivate) but that's not what I'm seeing in the vertical slice at the moment. Instead I see exposed CRUD methods that allow anyone (authenticated I assume?) to do anything. |
Creating a throwing away a temporary database doesn't give me the actual acceptance results considering we want to run this against live instances in the cloud - in which case that resetting option isn't really an option simply because we can't throw away our production database.
a lot of these non-business related CRUD operations endpoints will be private endpoints, hiding behind a middleware that requires particular roles, permissions and headers to be exposed and available only during particular test scenarios or reporting scenarios that team has to agree on, it's not free for all as some may think. I am not sure what the complexity is of the queries that you have used with EF Core, I can only speak from my experience with large datasets, EF Core seemed to be doing a good job, but that's a different discussion for a different day.
As I said, these operations some of them are private endpoints, exposed for particular cases, if hit from outside they return 404 as in the URL itself doesn't exist, I don't have any published materials about this yet, but hopefully I will blog/vlog about it when time allows. |
Right, I get it, there is a bigger picture that I am not seeing yet. I'll take you by your word that there is a bigger picture and these pieces will somehow fall into place. To be honest, at the moment all of this seems to me like an extremely expensive and unnecessarily complex test suite. I hope I'll get to eat my words with a side of humble pie down the line :-) |
@Driedas @hassanhabib with ORM the nice thing is, (if you avoid committing TX and/or using SaveChanges) that all your changes should be on the in memory ORM model. so you can do tests against! of course this works with some sample DB (or some seeding - even if in memory, or in a pre-seeded test DB). is this not worth to consider? There's one thing I did not really get, @hassanhabib wrote:
It wasn't clear, but I assume we're talking about a copy of the production database - as there should be no tests executed on a production database, because... So in this case it's just a copy you we're referring to. Now:
Ohh and EF core: still miss SQL side grouping support (at least with OData it sill failed, see: dotnet/efcore#10012) |
Hi everyone, |
@chaouanabil thanks for sharing - this framework looks interesting definitely something to consider. |
@hidegh I wasn't talking about a copy from a production database, I was talking about the actual database in production, running our acceptance tests against our production API and making sure we can run full cycle of CRUD operations (and orchestration, coordination, management and aggregation in the future) against the actual instance that everyone else is using. These tests we run are great, but they are limited by the place and time they are running within. it still doesn't report the overall health status of the instance that everyone else is using and that's what I want a status report about. Now, as of the grouping support and OData, I am still not sure where grouping fits in our operations - I won't be looking for a feature that I don't need yet to toss off an entire technology stack, if we go this way we won't be using any technologies at all and we will be building everything by hand. Will address other concerns as soon as I get the time, and I appreciate the feedback. |
Shutting this down due to inactivity, feel free to revive if you have feedback. |
I was wondering why are we throwing exceptions for validation cases? From what I have learned throwing exceptions for expected failures such as validation is not good practice. Or is throwing exceptions ok if we are catching them immediately? I do not question the architecture or decisions just want to know your thought on this @hassanhabib . Thank you. |
@devmanzur it depends on what you do with the exceptions. Does that make sense? |
I guess it makes sense for this context. Throwing exceptions makes the overall code simpler, But then we require creating a new exception type for each and every possible validation type. |
Awesome - please feel free to re-open if you have any further questions. |
@hassanhabib |
@youssefbennour I have been there, as you can see the last one to question this behavior 4 years ago was me :p. Result pattern is a valid approach and i use that too. Quoting the last sentence from @hassanhabib "Exceptions are just as import as part of our business logic as everything else, the only difference is that we throw them when we decide the entire operation is invalid and we require external action to restart the operation.". As you will see in the framework itself. you can either choose to use SingleAsync or SingleOrDefaultAsync. one throws exception straight away, another allows you to handle the null value and do as you like. You will find both of them are valid approach and it comes down to design decision. Exceptions are expensive but allowing user to continue when you encounter a invalid state might be more expensive. |
This issue is a thread to discuss potential patterns that we could adapt in OtripleS to streamline the development of the project, the current pattern follows the Models, Brokers, Services, Controllers and Views pattern, you can learn more about the pattern in the following sessions:
Fundamentals of Building ASP.NET Core Applications
Fundamentals of Building ASP.NET Core Applications (Brokers)
Fundamentals of Building ASP.NET Core Applications (Brokers & Services)
Architecting Industrial Software - Services Deep Dive
Exception Handling Noise in C#
Some of the contributors on this project have proposed some patterns like MediatR and some others had concerns about exception handling - this is the thread to discuss all these ideas and dive into their details.
This issue should not be a blocker for the current or future work until the team reaches a decision about which pattern we need to follow, it's never too late because eventually the current monolith we are building should dissolve into microservices this is the current mindset behind how we are building what we are building - building a monolith with microservices mindset.
The text was updated successfully, but these errors were encountered: