.Net Core was initially released in June 2016 as an improved framework on the previous .NET MVC framework (2009) that preceded the Webforms framework(2002).
- fast and open source compared to previous frameworks
- cross platform and no longer dependent on windows
- built in dependency injection(what is that ??) that allows apps to loosely couple with interfaces rather than specific implementations, making them easier to extend, maintain and test
- new versions easy update and are easy to keep up with
- cloud friendly (low memory and high-throughput - reliable, robust) and compatible with all cloud platforms
- improved performace compared to previous frameworks
- easily tested with automated tests: the loose coupling and support for dependency injection makes it easy to swap infrastructure concerns with fake implementations for test purposes. Also ships with a TestServer that can be used to host apps in memory
- supports both traditional and single page application (spa) behaviors.
- Traditional web apps depends on the server for all navigations, queries and updates the app might need with little client-side behavior. Each new user interaction sends a new web request, and requires page to reload to get results. typically followed in classic mvc frameworks
- single page applications(SPAs) involve very few dynamically generated server-side page loads. many SPAs are initialized with a static html file that loads the needed javascript functionalities for the interactivity, and make heavy use of web apis for their data needs. implemented in asp.net core with Blazor WebAssembly.
- many apps implement both behaviors and asp.net core supports both the provision of mvc and api functionalities in the same application
Traditional web apps perform most of the app logic on the server side while SPAs do so on the client side.
Use traditional web apps when:
- app's client side requirements are simple or read-only
- app need to function in browsers without javascript support
Use SPA when:
- app must expose a rich user interface with many features
- team is familiar with javascript, typescript or blazor webassemply development
- application must expose an api for other clients
Considerations for using spa:
- SPA frameworks require greater architectural and security expertise.
- susceptible to frequent updates and new client frameworks than traditonal web apps
- more challenging to configure automated build and deployment processes and utilize deployment options like containers
"If builders built buildings the way programmers wrote programs, then the first woodpecker that came along would destroy civilization". - Gerald Weinberg
When architecting and designing software applications, maintainability should be a core consideration.
- asserts that software should be separated based on the kind of work it performs.
- apps can be built to follow this principle by separating core business behavior from infrastructure and user-interface logic.
- ideally, business rules and logic should reside in a separate project, which should not depend on other projects in the application. i.e. having ui, business logic and database divisions within an application
- ensures that business model is easy to test and can evolve without being tightly-coupled to low-level implementation details
- achieved by the establishment of boundaries such as methods, objects, components and services to define core behavior within an application.
- more here
- principle of insulating different parts of an application from each other.
- app layers and components should be able to change their internal implementation without breaking their dependencies
- for example, in classes or objects, encapsulation is achieved by limiting outside access to the class's internal state, and allowing manipulations to its state to be made through a well-defined method or property setter.
- hiding the internal state of the object protects its integrity by preventing users from setting the internal data of the component into an invalid or inconsistent state.
- in C#, typical keywords, such as
public
,private
andprotected
offers a programmer a degree of control over what to hide in a class or interface implementation - for application components, encapsulation can be achieved by exposing well-defined interfaces for collaborators to use rather than allowing their state to be modified directly.
- read more here
- the direction of dependency within and application should be in the direction of the abstraction, not in the implementation details.
- high-level modules should not import anything from low-level modules. both should depend of abstractions such as interfaces
- abstractions should not depend on details. details/concrete implementations should depend on abstractions
- generally, when designing the interaction between a high-level and low-level module, they should not interact with each other directly, but rather through a layer of abstraction.
- this is not the case in traditional layers of dependency where one class references another directly when interacting thus creating a direct dependency graph and more complex run times.
- dependency inversion allows apps to be more loosely coupled, making them more testable, modular and maintainable
- read more here
- methods and classes should explicitly require any collaboration objects they need in order to function correctly
- when defining class constructors, any collaborating object that the class will need to function, should be explicitly called or stated in the constructor. the class should not have to depend on certain global or infrastructure components to function.
- classes with explicit dependencies are more honest, and tend to follow the Principle of Least Surprise by not affecting parts of the application they didn't explicitly demonstrate they needed to affect
- read more here
- objects should have only one responsibility and should have only one reason to change
- the only reason and object should change is if the manner in which it performs its one responsibility must be updated
- allows to produce more loosely coupled and modular systems, as individual classes are not overloaded with many implementation details
- application should avoid specifying behavior related to a particular concept in multiple places
- rather than duplicating logic, it makes sense to encapsulate such logic in a programming construct and making that construct the single authority for that behavior.
- classes modeling the business domain in a software application should not be impacted by how they might be persisted
- their design should focus on solving the business logic and should not be concerned with how the object's state is saved and later retrieved
- common violations include:
- domain objects that must inherit from a particular base class or expose certain properties
- a required interface implementation
- classes responsible for saving themselves
- required parameterless constructor
- properties requiring virtual keyword
-
a central pattern in domain driven developement that tackles complexity in large application by breaking it up into separate conceptual modules.
-
it's a strategic principle that provides demarcation for Ubiquitious Language(shared language spoken by all concerned parties) to keep ideas and concepts in context.
-
bounded contexts are not necessarily separated from one another
- entirely self-contained, in terms of its behaviour.
- may interact with other services or data stores, but core of its behavior runs within its own process
- entire application is usually deployed as a single unit
-
in this architecture, the entire logic of the application is contained in a single project, compiled to a single assembly, and deployed as a single unit.
-
a new ASP.NET Core project, starts out as a simple “all-in-one” monolith, and contains all the behavior for the application including its presentation, business, and data access logic.
-
in such an architecture, separation of concerns is achieved through the use of folders
-
major disadvantage is the separation of business logic into different folders (e.g.,models, views, controllers in mvc apps), making it difficult to which classes in which folders depends on which others
-
image below shows the file structure of a single-project app in visual studio
Layers As these applications get more complex, they evolve into multi-project applications where each project is considered to reside in a particular layer of the application according to its responsibilities or concerns. A layered approach makes it easy to reuse common low-level functionalities across the application. Applications can also enforce restrictions over which layers can communicate with other layers
-
application separated into 3 layers: user interface(UI), business logic layer(BLL) and data access layer(DAL)
-
users make requests through the UI, which only interacts with the BLL. BLL can in turn call the DAL for data access.
-
UI shouldn't make any request to the DAL directly nor should it interact with persistence (saved state) directly through other means.
-
BLL should only interact with persistence by going through the DAL
-
major con BLL is dependent on the existence of a database as compile dependencies run from top to bottowm (UI depends on BL which depends on DAL)
-
testing is also difficult as it requires a test database
In the image above, the solution structure has 3 projects - Application Core, Infrastructure and Web used to represent the BLL, DAL and UI respectively
-
follows both the dendency inversion principle and domain-driven design principles
-
puts the business logic and application model at the center of the application
-
instead of having business logic depend on data access or other infrastructure concerns, this dependency is inverted: infrastructure(DAL) and implementation details depend on the Application Core(BLL).
-
this achieved by defining abstractions, or interfaces, in the Application Core, which are then implemented by types defined in the Infrastructure layer.
-
ui layer works with interfaces defined in the application core at compile time, and ideally shouldn't know about the implementation types defined int he infrastructure layer.
-
however at run time, implementation types are required for app to execute, so they are served to the application core interfaces via dependency injection to be presented to the ui layer.
-
easier to write automated tests because application core doesn't depend on infrastructure.
simple clean architecture digram view. image source
- in the diagram below, the three layers are shown as three different projects in asp.net core, with each layer containing the various services, interfaces or entities needed for the application implementation.
asp.net core clean architecture implementation diagram image source
- Model: Represents the shape of the data, represented as classes. Corresponds to all the data related logic used in the application.
- View: Represents the user interface.
- Controller: Handles user requests and interfaces between the model and the view. Process all business logic.
- asp.net uses entity framework to build database models in applications
- supports LINQ queries, change tracking, updates and schema migrations.
- in asp.net, data annotations(attributes) are used to add context to the kind of fields defined in a model class
- The code snippet below shows a model class with the data annotations
Key
andRequired
added to give context to the Id and Name fieldspublic class Category { [Key] // Key data annotation(attribute) sets the Id column as the primary key and identity column public int Id { get; set; } [Required] // sets Name as a required field public int Name { get; set; } public int DisplayOrder { get; set; } public DateTime CreatedDateTime { get; set; } = DateTime.Now; // sets DateTime.Now as default value }
-
a dbcontext instance creates a session with a database to track changes, query and save instance of database entities.
-
lifetime of a
DbContext
begins when the instance is created and ends when the instance is disposed -
a
DbContext
instance is designed to be used for a single unit of work -
A Unit of Work keeps track of everything you do during a business transaction that can affect the database. When you're done, it figures out everything that needs to be done to alter the database as a result of your work
-
A unit of work using EF Core includes
- creation of a
DbContext
instance - tracking of entity instances by the context (need to read more on this)
- making of changes to tracked entities as needed to implement business rule
- calling of
SaveChanges
orSaveChangesAsync
to detect changes made and writing them to the database - disposing of instance
- creation of a
-
The idea behind using dbcontext to track changes to entities is to avoid writing directly to the database anytime changes are made
-
In many web apps, each http request represents a single unit of work, hence makes sense to tie the dbcontext lifetime to that of the request
-
In Asp.net,
ApplicationDbContext
, which is a subclass ofDbContext
is created for each request and passed to the controller to perform a unit of work before being disposed when the request ends. -
In the code snippet below, the
ApplicationDbContext
class inherits from the baseDbContext
class Within the constructor of the same name, the options defined in the custom class are passed down to the basedbcontext
class. -
To create an interface of the database entity
Category
aDbSet
of the entity namedCategories
is created with getters and setters. -
The
Categories
dbset can now be called in controllers with the application dbcontext to interact with the database.
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base (options)
// DbContextOptions<ApplicationDbContext> is named options and passes down the options defined in the
// ApplicationDbcontext to the base DbContext class
{
}
public DbSet<Category> Categories { get; set; }
// this creates a Category table with the name Categories based on the
// model definition in the Category class
}
- Migrations help track code changes to data models and implement them to the database
- Process to making migrations
- in the NuGet package manager console, install the
Microsoft.EntityFrameworkCore.Tools
package to enable migration commands - you can then run
add-migration <migration name>
in the package manager console - this creates a migration folder with a migration class file that contains
Up
andDown
methods for upgrading and downgrading database based on changes to the model class - use the update-database command to push the migration to the database
- entity framework core uses the logic in the migration class to generate and insert sql queries into the database to make the necessary database changes
- in the NuGet package manager console, install the
- add a database connection string to the
ConnectionStrings
block of theappsettings.json
file. database connection string should contain name of server, database and a trusted_connection boolean - congifure the application dbcontext to open a session to the database, track changes and make modifications to the database.
this is done by creating an
ApplicationDbContext
class which subclassesDbContext
with various options. the application db context class will hold the names of all the database entities defined using theDbSet
class - add the created application context to the application services using
builder.Services.AddDbContext()
method in the Program.cs file. pass the database management system being used as an option to theAddDbContext()
method along with the connection string created in theappsettings.json
file. - make migrations to the database to create the entities defined in the application
Controllers handle the application logic and interfaces between the database and the view(user interface).
In an MVC architecture, separate controller classes are usually created to handle related business logic units.
When a Controller class is created, it inherits from the base Mvc.Controller
class which makes it easy to write controller
logic without writing boilerplate code.
-
To interact with the database within a controller, we create a reference to the application dbcontext in the controller class.
-
A unit of work within a controller are represented by "action" methods.
-
Action methods perform a unit of work and then can return a result such as a view, partial view, redirect, json, file, etc.
-
Action methods can use various inbuilt interfaces such as
IActionResult
,ViewResult
, etc. to return various "results" after an action has been completed. -
IActionResult
is a generic type that implements all other return types and is appropriate when different return types are possible in one action -
but a more specific class like
ViewResult
can be used when a view is specifically being returnedpublic class GenericController : Controller { // using the generic IActionResult public IActionResult Index() { return View() } //using ViewResult public ViewResult Index() { return View() } }
- razor pages
- layouts and partials
- default bootstrap
- enables server-side code to participate in creating and rendering HTML elements in Razor files.
- pretty much like django template tags
-
To automatically associate a controller action with a view(html page) create the view page with the name of the associated action within a subfolder with the controller name in the view folder. For example, an action with the name
Index
under theCategory
controller will expect its view file to be found at/Views/Category/Index.cshtml
unless otherwise stated -
To allow a user to navigate to a view page, add a link to the view page from another page, by specifying the name of the controller and action associated with the view within the
<a>
tag.<li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Category" asp-action="Index">Category</a> </li>
-
In the code snippet below the tag helpers
asp-controller
andasp-action
are used to indicate the controller and action that should be contacted once user clicks on the link named 'Category'
-
Reading data from a database and rendering it in a view requires using the application dbcontext and controller action to interface between the view and the database.
-
In the controller class, access the dbset defined in the application dbcontext to get access to the database entity objects.
-
Typical read operations include:
- getting a list of items from a database entity
- searching for a specific item within a database
-
In the code snippet below, within the
Index()
action method, we have defined anIEnumberable
objectobjCategoryList
. This object contains an enumerable list of all items in the category model. -
This is read from the
Categories
dbset in the application dbcontext as_db.Categories
. -
The result of the data read (
objCategoryList
) is then passed to the and returned with theView()
function.public class CategoryController : Controller { private readonly ApplicationDbContext _db; public CategoryController(ApplicationDbContext db) { _db = db; } public IActionResult Index() { IEnumerable<Category> objCategoryList = _db.Categories; return View(objCategoryList); } }
-
At the top of the html page, using the
@model
directive with the model name allows the view to accept the model object that the controller passed to the view from theIndex
action method.
// in .cshtml file
@model IEnumerable<Category>
The code snippet below in the view page tells the view to expect an IEnumerable Category model object to be passed to it
When writing to a database in a create action, we still acess the database entity from the application db context just as we'd do in the read action. However, the major change comes from how we interact with the database. In a create action, we append a new row to the database
- The asp.net core pipeline contains a sequence of functions(middlewares) and specifies how an application should respond to requests received.
- When a request passes through the midlleware pipeline, it goes through each function in a sequential order.
- Each function process the request and performs an action, then passes the processed request on to the next.
- When a response is also being sent back to the client, the response messages passes through a similar process in the pipeline, before being sent to the client.
- Requests received from the browser goes through the pipeline, which is made of different middlewares.
- MVC is a type of middleware itself and can be added to an application by adding the
AddControllersWithViews()
method to the application builder services. - This adds a number of MVC focused services to the app builder.
- Other middleware include Authentication, Authorization, Routing and Static Files
- The order in which middleware is added to the application pipeline is extremely important as it dictates how the request will be passed.
- For example, authentication middleware should always come before authorization middleware
- mvc apps can use conventional routes, attribute routes, or both.
- conventional routes are defined in code, specifying routing conventions such as below:
app.UseEndpoints(endpoints => { endpoints.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); });
- this route defined will yield this url on a locaholhost:5555(for example) domain:
https://localhost:5555(domain)/Home/Index/3
- convention states that first part of the route should correspond with the related controller, second part to the related action with an optional id pararmeter at the third part.
- routes are defined in the program.cs file alongside the other middleware configurations
- In case those are not provided in a url pattern, the default controllers and actions defined in the program will be used.
Authentication options
- Custom: you can create a custom authentication middleware that handles all auth requests without using any default .net auth middleware or services
- Authentication middleware: you add the built-in dotnet authentication middleware to your middleware pipeline by using
app.UseAuthentication()
whereapp
represents aWebApplication.CreateBuilder().Build()
instance. - Authentication service: you can use the inbuilt dotnet core identity service by adding
AAddAuthentication()
to the program services defined inProgram.cs
- more here
TempData
in .net core allows the storing of data that persists for only one request.- Makes it appropriate to store data such as alerts and messages
- When naming partial a partial view, it's best practice to start it's name with an underscore. e.g.
_Layout.cshtml
- Use PascalCase when when naming a class, record, or struct.
- When naming an interface, use PascalCase in addition to prefixing the name with an 'I'
- When naming public members of types, such as fields, properties, events, methods, and local functions, use pascal casing
- Use camel casing ("camelCasing") when naming private or internal fields, and prefix them with _.
- When working with static fields that are private or internal, use the s_ prefix and for thread static use t_.
- When writing method parameters, use camel casing.
- more conventions