Merge branch 'main' into dotnet/net-7

This commit is contained in:
Marcello 2022-06-06 19:02:08 +02:00
commit 8c6c9ac4a4
134 changed files with 554 additions and 1500 deletions

View file

@ -0,0 +1,244 @@
# ASP.NET Configuration
## `.csproj`
```xml
<PropertyGroup>
<!-- enable documentation comments (can be used for swagger) -->
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- do not warn public classes w/o documentation comments -->
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
```
## `Program.cs`
```cs
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
namespace App
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run(); // start and config ASP.NET Core App
// or start Blazor WASM Single Page App
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();
}
// for MVC, Razor Pages and Blazor Server
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>(); // config handled in Startup.cs
});
}
}
```
## `Startup.cs`
```cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace App
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the DI container.
public void ConfigureServices(IServiceCollection services)
{
// set db context for the app using the connection string
services.AddDbContext<AppDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
// Captures synchronous and asynchronous Exception instances from the pipeline and generates HTML error responses.
services.AddDatabaseDeveloperPageExceptionFilter();
// use Razor Pages, runtime compilation needs Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation pkg
services.AddRazorPages().AddRazorRuntimeCompilation();
// or
services.AddControllers(); // controllers w/o views
//or
services.AddControllersWithViews(); // MVC Controllers
//or
services.AddServerSideBlazor(); // needs Razor Pages
services.AddSignalR();
// set dependency injection lifetimes
services.AddSingleton<ITransientService, ServiceImplementation>();
services.AddScoped<ITransientService, ServiceImplementation>();
services.AddTransient<ITransientService, ServiceImplementation>();
// add swagger
services.AddSwaggerGen(options => {
// OPTIONAL: use xml comments for swagger documentation
var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename));
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseSwagger();
app.UseSwaggerUI();
app.UseEndpoints(endpoints =>
{
// MVC routing
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}"
);
// or
endpoints.MapControllers(); // map controllers w/o views
// or
endpoints.MapRazorPages();
// or
endpoints.MapBlazorHub(); // SignalR Hub for Blazor Server
endpoints.MapHub("/hub/endpoint"); // SignalR Hub
endpoints.MapFallbackToPage("/_Host"); // fallback for razor server
});
}
}
}
```
## Application Settings
App settings are loaded (in order) from:
1. `appsettings.json`
2. `appsettings.<Environment>.json`
3. User Secrets
The environment is controlled by the env var `ASPNETCORE_ENVIRONMENT`. If a setting is present in multiple locations, the last one is used and overrides the previous ones.
### User Secrets
User secrets are specific to each machine and can be initialized with `dotnet user-secrets init`. Each application is linked with it's settings by a guid.
The settings are stored in:
- `%APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json` (Windows)
- `~/.microsoft/usersecrets/<user_secrets_id>/secrets.json` (Linux/macOS)
Setting a value is done with `dotnet user-secrets set <key> <value>`, keys can be nested by separating each level with `:` or `__`.
## Options Pattern
The *options pattern* uses classes to provide strongly-typed access to groups of related settings.
```json
{
"SecretKey": "Secret key value",
"TransientFaultHandlingOptions": {
"Enabled": true,
"AutoRetryDelay": "00:00:07"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
```
```cs
// options model for binding
public class TransientFaultHandlingOptions
{
public bool Enabled { get; set; }
public TimeSpan AutoRetryDelay { get; set; }
}
```
```cs
// setup the options
builder.Services.Configure<TransientFaultHandlingOptions>(builder.Configuration.GetSection<TransientFaultHandlingOptions>(nameof(Options)));
builder.Services.Configure<TransientFaultHandlingOptions>(builder.Configuration.GetSection<TransientFaultHandlingOptions>(key));
```
```cs
class DependsOnOptions
{
private readonly IOptions<TransientFaultHandlingOptions> _options;
public DependsOnOptions(IOptions<TransientFaultHandlingOptions> options) => _options = options;
}
```
### [Options interfaces](https://docs.microsoft.com/en-us/dotnet/core/extensions/options#options-interfaces)
`IOptions<TOptions>`:
- Does not support:
- Reading of configuration data after the app has started.
- Named options
- Is registered as a Singleton and can be injected into any service lifetime.
`IOptionsSnapshot<TOptions>`:
- Is useful in scenarios where options should be recomputed on every injection resolution, in scoped or transient lifetimes.
- Is registered as Scoped and therefore cannot be injected into a Singleton service.
- Supports named options
`IOptionsMonitor<TOptions>`:
- Is used to retrieve options and manage options notifications for `TOptions` instances.
- Is registered as a Singleton and can be injected into any service lifetime.
- Supports:
- Change notifications
- Named options
- Reloadable configuration
- Selective options invalidation (`IOptionsMonitorCache<TOptions>`)

View file

@ -0,0 +1,416 @@
# Blazor
Blazor apps are based on *components*. A **component** in Blazor is an element of UI, such as a page, dialog, or data entry form.
Components are .NET C# classes built into .NET assemblies that:
- Define flexible UI rendering logic.
- Handle user events.
- Can be nested and reused.
- Can be shared and distributed as Razor class libraries or NuGet packages.
![Blazor Server Architecture](../../img/dotnet_blazor-server.png)
![Blazor WASM Architecture](../../img/dotnet_blazor-webassembly.png)
The component class is usually written in the form of a Razor markup page with a `.razor` file extension. Components in Blazor are formally referred to as *Razor components*.
## Project Structure & Important Files
### Blazor Server Project Structure
```txt
Project
|-Properties
| |- launchSettings.json
|
|-wwwroot --> static files
| |-css
| | |- site.css
| | |- bootstrap
| |
| |- favicon.ico
|
|-Pages
| |- _Host.cshtml --> fallback page
| |- Component.razor
| |- Index.razor
| |- ...
|
|-Shared
| |- MainLayout.razor
| |- MainLayout.razor.css
| |- ...
|
|- _Imports.razor --> @using imports
|- App.razor --> component root of the app
|
|- appsettings.json --> application settings
|- Program.cs --> App entry-point
|- Startup.cs --> services and middleware configs
```
### Blazor WASM Project Structure
```txt
Project
|-Properties
| |- launchSettings.json
|
|-wwwroot --> static files
| |-css
| | |- site.css
| | |- bootstrap
| |
| |- index.html
| |- favicon.ico
|
|-Pages
| |- Component.razor
| |- Index.razor
| |- ...
|
|-Shared
| |- MainLayout.razor
| |- MainLayout.razor.css
| |- ...
|
|- _Imports.razor --> @using imports
|- App.razor --> component root of the app
|
|- appsettings.json --> application settings
|- Program.cs --> App entry-point
```
### Blazor PWA Project Structure
```txt
Project
|-Properties
| |- launchSettings.json
|
|-wwwroot --> static files
| |-css
| | |- site.css
| | |- bootstrap
| |
| |- index.html
| |- favicon.ico
| |- manifest.json
| |- service-worker.js
| |- icon-512.png
|
|-Pages
| |- Component.razor
| |- Index.razor
| |- ...
|
|-Shared
| |- MainLayout.razor
| |- MainLayout.razor.css
| |- ...
|
|- _Imports.razor --> @using imports
|- App.razor --> component root of the app
|
|- appsettings.json --> application settings
|- Program.cs --> App entrypoint
```
### `manifest.json`, `service-worker.js` (Blazor PWA)
[PWA](https://web.dev/progressive-web-apps/)
[PWA MDN Docs](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps)
[PWA Manifest](https://developer.mozilla.org/en-US/docs/Web/Manifest)
[Service Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API)
```json
// manifest.json
{
"name": "<App Name>",
"short_name": "<Short App Name>",
"start_url": "./",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#03173d",
"icons": [
{
"src": "icon-512.png",
"type": "image/png",
"sizes": "512x512"
}
]
}
```
## Common Blazor Files
### `App.razor`
```cs
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
```
### `MainLayout.razor` (Blazor Server/WASM)
```cs
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu /> // NavMenu Component
</div>
<div class="main">
<div class="top-row px-4">
</div>
<div class="content px-4">
@Body
</div>
</div>
</div>
```
### `_Host.cshtml` (Blazor Server)
```html
@page "/"
@namespace BlazorServerDemo.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
Layout = null;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BlazorServerDemo</title>
<base href="~/" />
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
<link href="css/site.css" rel="stylesheet" />
<link href="BlazorServerDemo.styles.css" rel="stylesheet" />
</head>
<body>
<component type="typeof(App)" render-mode="ServerPrerendered" />
<div id="blazor-error-ui">
<environment include="Staging,Production">
An error has occurred. This application may no longer respond until reloaded.
</environment>
<environment include="Development">
An unhandled exception has occurred. See browser dev tools for details.
</environment>
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.server.js"></script>
</body>
</html>
```
## Components (`.razor`)
[Blazor Components](https://docs.microsoft.com/en-us/aspnet/core/blazor/components/)
```cs
@page "/route/{RouteParameter}" // make component accessible from a URL
@page "/route/{RouteParameter?}" // specify route parameter as optional
@page "/route/{RouteParameter:<type>}" // specify route parameter type
@namespace <Namespace> // set the component namespace
@using <Namespace> // using statement
@inherits BaseType // inheritance
@attribute [Attribute] // apply an attribute
@inject Type objectName // dependency injection
// html of the page here
<Namespace.ComponentFolder.Component /> // access component w/o @using
<Component Property="value"/> // insert component into page, passing attributes
<Component @onclick="@CallbackMethod">
@ChildContent // segment of UI content
</Component>
@code {
// component model (Properties, Methods, ...)
[Parameter] // capture attribute
public Type Property { get; set; } = defaultValue;
[Parameter] // capture route parameters
public type RouteParameter { get; set;}
[Parameter] // segment of UI content
public RenderFragment ChildContent { get; set;}
private void CallbackMethod() { }
}
```
## State Management
### Blazor WASM
```cs
// setup state singleton
builder.Services.AddSingleton<StateContainer>();
```
```cs
// StateContainer singleton
using System;
public class StateContainer
{
private int _counter;
public string Property
{
get => _counter;
set
{
_counter = value;
NotifyStateChanged(); // will trigger StateHasChanged(), causing a render
}
}
public event Action OnChange;
private void NotifyStateChanged() => OnChange?.Invoke();
}
```
```cs
// component that changes the state
@inject StateContainer State
// Delegate event handlers automatically trigger a UI render
<button @onClick="@HandleClick">
Change State
</button>
@code {
private void HandleClick()
{
State.Property += 1; // update state
}
}
```
```cs
// component that should be update on state change
@implements IDisposable
@inject StateContainer State
<p>Property: <b>@State.Property</b></p>
@code {
// StateHasChanged notifies the component that its state has changed.
// When applicable, calling StateHasChanged causes the component to be rerendered.
protected override void OnInitialized()
{
State.OnChange += StateHasChanged;
}
public void Dispose()
{
State.OnChange -= StateHasChanged;
}
}
```
## Data Binding & Events
```cs
<p>
<button @on{DOM EVENT}="{DELEGATE}" />
<button @on{DOM EVENT}="{DELEGATE}" @on{DOM EVENT}:preventDefault /> // prevent default action
<button @on{DOM EVENT}="{DELEGATE}" @on{DOM EVENT}:preventDefault="{CONDITION}" /> // prevent default action if CONDITION is true
<button @on{DOM EVENT}="{DELEGATE}" @on{DOM EVENT}:stopPropagation />
<button @on{DOM EVENT}="{DELEGATE}" @on{DOM EVENT}:stopPropagation="{CONDITION}" /> // stop event propagation if CONDITION is true
<button @on{DOM EVENT}="@(e => Property = value)" /> // change internal state w/ lambda
<button @on{DOM EVENT}="@(e => DelegateAsync(e, value))" /> // invoke delegate w/ lambda
<input @ref="elementReference" />
<input @bind="{PROPERTY}" /> // updates variable on ONCHANGE event (focus loss)
<input @bind="{PROPERTY}" @bind:event="{DOM EVENT}" /> // updates value on DOM EVENT
<input @bind="{PROPERTY}" @bind:format="{FORMAT STRING}" /> // use FORMAT STRING to display value
<ChildComponent @bind-{PROPERTY}="{PROPERTY}" @bind-{PROPERTY}:event="{EVENT}" /> // bind to child component {PROPERTY}
<ChildComponent @bind-{PROPERTY}="{PROPERTY}" @bind-{PROPERTY}:event="{PROPERTY}Changed" /> // bind to child component {PROPERTY}, listen for custom event
</p>
@code {
private ElementReference elementReference;
public string Property { get; set; }
public EventCallback<Type> PropertyChanged { get; set; } // custom event {PROPERTY}Changed
// invoke custom event
public async Task DelegateAsync(EventArgs e, Type argument)
{
/* ... */
await PropertyChanged.InvokeAsync(e, argument); // notify parent bound prop has changed
await elementReference.FocusAsync(); // focus an element in code
}
}
```
**NOTE**: When a user provides an unparsable value to a data-bound element, the unparsable value is automatically reverted to its previous value when the bind event is triggered.
## Javascript/.NET Interop
[Call Javascript from .NET](https://docs.microsoft.com/en-us/aspnet/core/blazor/call-javascript-from-dotnet)
[Call .NET from Javascript](https://docs.microsoft.com/en-us/aspnet/core/blazor/call-dotnet-from-javascript)
### Render Blazor components from JavaScript [C# 10]
To render a Blazor component from JavaScript, first register it as a root component for JavaScript rendering and assign it an identifier:
```cs
// Blazor Server
builder.Services.AddServerSideBlazor(options =>
{
options.RootComponents.RegisterForJavaScript<Counter>(identifier: "counter");
});
// Blazor WebAssembly
builder.RootComponents.RegisterForJavaScript<Counter>(identifier: "counter");
```
Load Blazor into the JavaScript app (`blazor.server.js` or `blazor.webassembly.js`) and then render the component from JavaScript into a container element using the registered identifier, passing component parameters as needed:
```js
let containerElement = document.getElementById('my-counter');
await Blazor.rootComponents.add(containerElement, 'counter', { incrementAmount: 10 });
```
### Blazor custom elements [C# 10]
Experimental support is also now available for building custom elements with Blazor using the Microsoft.AspNetCore.Components.CustomElements NuGet package.
Custom elements use standard HTML interfaces to implement custom HTML elements.
To create a custom element using Blazor, register a Blazor root component as custom elements like this:
```cs
options.RootComponents.RegisterAsCustomElement<Counter>("my-counter");
```

View file

@ -0,0 +1,132 @@
# [Filters](https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/filters)
**Filters** in ASP.NET Core allow code to be run _before_ or _after_ specific stages in the request processing pipeline.
Built-in filters handle tasks such as:
- Authorization (preventing access to resources a user isn't authorized for).
- Response caching (short-circuiting the request pipeline to return a cached response).
Custom filters can be created to handle cross-cutting concerns. Examples of cross-cutting concerns include error handling, caching, configuration, authorization, and logging. Filters avoid duplicating code.
## **How filters work**
Filters run within the _ASP.NET Core action invocation pipeline_, sometimes referred to as the _filter pipeline_. The filter pipeline runs after ASP.NET Core selects the action to execute.
![filter-pipeline-1](../../img/dotnet_filter-pipeline-1.png)
![filter-pipeline-2](../../img/dotnet_filter-pipeline-2.png)
## **Filter types**
Each filter type is executed at a different stage in the filter pipeline:
- **Authorization filters** run first and are used to determine whether the user is authorized for the request. Authorization filters short-circuit the pipeline if the request is not authorized.
- **Resource filters**:
- Run after authorization.
- `OnResourceExecuting` runs code before the rest of the filter pipeline. For example, `OnResourceExecuting` runs code before model binding.
- `OnResourceExecuted` runs code after the rest of the pipeline has completed.
- **Action filters**:
- Run code immediately before and after an action method is called.
- Can change the arguments passed into an action.
- Can change the result returned from the action.
- Are **not** supported in Razor Pages.
- **Exception filters** apply global policies to unhandled exceptions that occur before the response body has been written to.
- **Result filters** run code immediately before and after the execution of action results. They run only when the action method has executed successfully. They are useful for logic that must surround view or formatter execution.
## **Implementation**
Filters support both synchronous and asynchronous implementations through different interface definitions.
For example, `OnActionExecuting` is called before the action method is called. `OnActionExecuted` is called after the action method returns.
Asynchronous filters define an `On-Stage-ExecutionAsync` method, for example `OnActionExecutionAsync`.
Interfaces for multiple filter stages can be implemented in a single class.
## **Built-in filter attributes**
ASP.NET Core includes built-in _attribute-based_ filters that can be subclassed and customized.
Several of the filter interfaces have corresponding attributes that can be used as base classes for custom implementations.
Filter attributes:
- [ActionFilterAttribute](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionfilterattribute)
- [ExceptionFilterAttribute](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.exceptionfilterattribute)
- [ResultFilterAttribute](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.resultfilterattribute)
- [FormatFilterAttribute](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatfilterattribute)
- [ServiceFilterAttribute](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.servicefilterattribute)
- [TypeFilterAttribute](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.typefilterattribute)
## **Filter scopes**
A filter can be added to the pipeline at one of three *scopes*:
- Using an attribute on a controller action. Filter attributes cannot be applied to Razor Pages handler methods.
```cs
// services.AddScoped<CustomActionFilterAttribute>();
[ServiceFilter(typeof(CustomActionFilterAttribute))]
public IActionResult Index()
{
return Content("Header values by configuration.");
}
```
- Using an attribute on a controller or Razor Page.
```cs
// services.AddControllersWithViews(options => { options.Filters.Add(new CustomResponseFilterAttribute(args)); });
[CustomResponseFilterAttribute(args)]
public class SampleController : Controller
// or
[CustomResponseFilterAttribute(args)]
[ServiceFilter(typeof(CustomActionFilterAttribute))]
public class IndexModel : PageModel
```
- Globally for all controllers, actions, and Razor Pages.
```cs
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews(options =>
{
options.Filters.Add(typeof(CustomActionFilter));
});
}
```
## Filter Order of Execution
When there are multiple filters for a particular stage of the pipeline, scope determines the default order of filter execution. Global filters surround class filters, which in turn surround method filters.
As a result of filter nesting, the *after* code of filters runs in the reverse order of the *before* code. The filter sequence:
- The *before* code of global filters.
- The *before* code of controller and Razor Page filters.
- The *before* code of action method filters.
- The *after* code of action method filters.
- The *after* code of controller and Razor Page filters.
- The *after* code of global filters.
### Cancellation and Short-Circuiting
The filter pipeline can be short-circuited by setting the `Result` property on the `ResourceExecutingContext` parameter provided to the filter method.
```cs
public class ShortCircuitingResourceFilterAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
context.Result = new ContentResult()
{
Content = "Resource unavailable - header not set."
};
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
}
}
```

View file

@ -0,0 +1,207 @@
# [Middleware](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware)
Middleware is software that's assembled into an app pipeline to handle requests and responses. Each component:
- Chooses whether to pass the request to the next component in the pipeline.
- Can perform work before and after the next component in the pipeline.
Request delegates are used to build the request pipeline. The request delegates handle each HTTP request.
Request delegates are configured using [Run][Run_docs], [Map][Map_docs], and [Use][Use_docs] extension methods.
An individual request delegate can be specified in-line as an anonymous method (called in-line middleware), or it can be defined in a reusable class.
These reusable classes and in-line anonymous methods are *middleware*, also called *middleware components*.
Each middleware component in the request pipeline is responsible for invoking the next component in the pipeline or short-circuiting the pipeline.
When a middleware short-circuits, it's called a *terminal middleware* because it prevents further middleware from processing the request.
[Use_docs]: https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.useextensions.use
[Run_docs]: https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.runextensions.run
[Map_docs]: https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.mapextensions.map
## Middleware Pipeline
The ASP.NET Core request pipeline consists of a sequence of request delegates, called one after the other.
![request-delegate-pipeline](../../img/dotnet_request-delegate-pipeline.png)
Each delegate can perform operations before and after the next delegate. Exception-handling delegates should be called early in the pipeline, so they can catch exceptions that occur in later stages of the pipeline. It's possible to chain multiple request delegates together with `Use`.
The *next* parameter represents the next delegate in the pipeline. It's possible to short-circuit the pipeline by *not calling* the next parameter.
When a delegate doesn't pass a request to the next delegate, it's called *short-circuiting the request pipeline*.
Short-circuiting is often desirable because it avoids unnecessary work.
It's possible to perform actions both *before* and *after* the next delegate:
```cs
public class Startup
{
public void Configure(IApplicationBuilder app)
{
// "inline" middleware, best if in own class
app.Use(async (context, next) =>
{
// Do work that doesn't write to the Response.
await next.Invoke();
// Do logging or other work that doesn't write to the Response.
});
}
}
```
`Run` delegates don't receive a next parameter. The first `Run` delegate is always terminal and terminates the pipeline.
```cs
public class Startup
{
public void Configure(IApplicationBuilder app)
{
// "inline" middleware, best if in own class
app.Use(async (context, next) =>
{
// Do work that doesn't write to the Response.
await next.Invoke();
// Do logging or other work that doesn't write to the Response.
});
app.Run(async context =>
{
// no invocation of next
});
}
}
```
## Middleware Order
![middleware-pipeline](../../img/dotnet_middleware-pipeline.png)
![mvc-endpoint](../../img/dotnet_mvc-endpoint.png)
The Endpoint middleware executes the filter pipeline for the corresponding app type.
The order that middleware components are added in the `Startup.Configure` method defines the order in which the middleware components are invoked on requests and the reverse order for the response. The order is **critical** for security, performance, and functionality.
```cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
// app.UseCookiePolicy();
app.UseRouting();
// app.UseRequestLocalization();
// app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
// app.UseSession();
// app.UseResponseCompression();
// app.UseResponseCaching();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
```
[Built-in Middleware](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/#built-in-middleware)
## Branching the Middleware Pipeline
`Map` extensions are used as a convention for branching the pipeline. `Map` branches the request pipeline based on matches of the given request path.
If the request path starts with the given path, the branch is executed.
When `Map` is used, the matched path segments are removed from `HttpRequest.Path` and appended to `HttpRequest.PathBase` for each request.
`MapWhen` branches the request pipeline based on the result of the given predicate.
Any *predicate* of type `Func<HttpContext, bool>` can be used to map requests to a new branch of the pipeline.
`UseWhen` also branches the request pipeline based on the result of the given predicate.
Unlike with `MapWhen`, this branch is rejoined to the main pipeline if it doesn't short-circuit or contain a terminal middleware.
## Custom Middleware Classes
Middleware is generally encapsulated in a class and exposed with an extension method.
```cs
using Microsoft.AspNetCore.Http;
using System.Globalization;
using System.Threading.Tasks;
namespace <App>
{
public class CustomMiddleware
{
private readonly RequestDelegate _next;
public CustomMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
// Do work that doesn't write to the Response.
await _next(context); // Call the next delegate/middleware in the pipeline
// Do logging or other work that doesn't write to the Response.
}
}
}
```
The middleware class **must** include:
- A public constructor with a parameter of type [RequestDelegate][RequestDelegate_docs].
- A public method named `Invoke` or `InvokeAsync`. This method must:
- Return a `Task`.
- Accept a first parameter of type [HttpContext][HttpConrext_Docs].
[RequestDelegate_docs]: https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.requestdelegate
[HttpConrext_Docs]: https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext
## Middleware Extension Methods
```cs
using Microsoft.AspNetCore.Builder;
namespace <App>
{
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseCustom(this IApplicationBuilder builder)
{
return builder.UseMiddleware<CustomMiddleware>();
}
}
}
```
```cs
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// other middlewares
app.UseCustom(); // add custom middleware in the pipeline
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
```

View file

@ -0,0 +1,252 @@
# Minimal API
**NOTE**: Requires .NET 6+
```cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IService, Service>();
builder.Services.AddScoped<IService, Service>();
builder.Services.AddTransient<IService, Service>();
var app = builder.Build();
// [...]
app.Run();
//or
app.RunAsync();
```
## Swagger
```cs
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// [...]
app.UseSwagger();
app.UseSwaggerUI();
// add returned content metadata to Swagger
app.MapGet("/route", Handler).Produces<Type>(statusCode);
// add request body contents metadata to Swagger
app.MapPost("/route", Handler).Accepts<Type>(contentType);
```
## MVC
```cs
builder.Services.AddControllersWithViews();
//or
builder.Services.AddControllers();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
```
## Routing, Handlers & Results
To define routes and handlers using Minimal APIs, use the `Map(Get|Post|Put|Delete)` methods.
```cs
// the dependencies are passed as parameters in the handler delegate
app.MapGet("/route/{id}", (IService service, int id) => {
return entity is not null ? Results.Ok(entity) : Results.NotFound();
});
// pass delegate to use default values
app.MapGet("/search/{id}", Search);
IResult Search(int id, int? page = 1, int? pageSize = 10) { /* ... */ }
```
### Route Groups
The `MapGroup()` extension method, which helps organize groups of endpoints with a common prefix.
It allows for customizing entire groups of endpoints with a singe call to methods like `RequireAuthorization()` and `WithMetadata()`.
```cs
var group = app.MapGroup("<route-prefix>");
group.MapGet("/", GetAllTodos); // route: /<route-prefix>
group.MapGet("/{id}", GetTodo); // route: /<route-prefix>/{id}
// [...]
```
### `TypedResults`
The `Microsoft.AspNetCore.Http.TypedResults` static class is the “typed” equivalent of the existing `Microsoft.AspNetCore.Http.Results` class.
It's possible to use `TypedResults` in minimal APIs to create instances of the in-framework `IResult`-implementing types and preserve the concrete type information.
```cs
public static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
```
```cs
[Fact]
public async Task GetAllTodos_ReturnsOkOfObjectResult()
{
// Arrange
var db = CreateDbContext();
// Act
var result = await TodosApi.GetAllTodos(db);
// Assert: Check the returned result type is correct
Assert.IsType<Ok<Todo[]>>(result);
}
```
### Multiple Result Types
The `Results<TResult1, TResult2, TResultN>` generic union types, along with the `TypesResults` class, can be used to declare that a route handler returns multiple `IResult`-implementing concrete types.
```cs
// Declare that the lambda returns multiple IResult types
app.MapGet("/todos/{id}", async Results<Ok<Todo>, NotFound> (int id, TodoDb db)
{
return await db.Todos.FindAsync(id) is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound();
});
```
## Filters
```cs
public class ExampleFilter : IRouteHandlerFilter
{
public async ValueTask<object?> InvokeAsync(RouteHandlerInvocationContext context, RouteHandlerFilterDelegate next)
{
// before endpoint call
var result = next(context);
/// after endpoint call
return result;
}
}
```
```cs
app.MapPost("/route", Handler).AddFilter<ExampleFilter>();
```
## Context
With Minimal APIs it's possible to access the contextual information by passing one of the following types as a parameter to your handler delegate:
- `HttpContext`
- `HttpRequest`
- `HttpResponse`
- `ClaimsPrincipal`
- `CancellationToken` (RequestAborted)
```cs
app.MapGet("/hello", (ClaimsPrincipal user) => {
return "Hello " + user.FindFirstValue("sub");
});
```
## OpenAPI
The `Microsoft.AspNetCore.OpenApi` package exposes a `WithOpenApi` extension method that generates an `OpenApiOperation` derived from a given endpoints route handler and metadata.
```cs
app.MapGet("/todos/{id}", (int id) => ...)
.WithOpenApi();
app.MapGet("/todos/{id}", (int id) => ...)
.WithOpenApi(operation => {
operation.Summary = "Retrieve a Todo given its ID";
operation.Parameters[0].AllowEmptyValue = false;
});
```
## Validation
Using [Minimal Validation](https://github.com/DamianEdwards/MinimalValidation) by Damian Edwards.
Alternatively it's possible to use [Fluent Validation](https://fluentvalidation.net/).
```cs
app.MapPost("/widgets", (Widget widget) => {
var isValid = MinimalValidation.TryValidate(widget, out var errors);
if(isValid)
{
return Results.Created($"/widgets/{widget.Name}", widget);
}
return Results.BadRequest(errors);
});
class Widget
{
[Required, MinLength(3)]
public string? Name { get; set; }
public override string? ToString() => Name;
}
```
## JSON Serialization
```cs
// Microsoft.AspNetCore.Http.Json.JsonOptions
builder.Services.Configure<JsonOptions>(opt =>
{
opt.SerializerOptions.PropertyNamingPolicy = new SnakeCaseNamingPolicy();
});
```
## Authorization
```cs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer();
builder.Services.AddAuthorization();
// or
builder.Services.AddAuthorization(options =>
{
// for all endpoints
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser();
})
// [...]
app.UseAuthentication();
app.UseAuthorization(); // must come before routes
// [...]
app.MapGet("/alcohol", () => Results.Ok()).RequireAuthorization("<policy>"); // on specific endpoints
app.MapGet("/free-for-all", () => Results.Ok()).AllowAnonymous();
```

259
docs/dotnet/asp.net/mvc.md Normal file
View file

@ -0,0 +1,259 @@
# ASP.NET (Core) MVC Web App
## Project Structure
```txt
Project
|-Properties
| |- launchSettings.json
|
|-wwwroot --> location of static files
| |-css
| | |- site.css
| |
| |-js
| | |- site.js
| |
| |-lib
| | |- bootstrap
| | |- jquery
| | |- ...
| |
| |- favicon.ico
|
|-Model
| |-ErrorViewModel.cs
| |- Index.cs
| |-...
|
|-Views
| |-Home
| | |- Index.cshtml
| |
| |-Shared
| | |- _Layout.cshtml --> reusable default page layout
| | |- _ValidationScriptsPartial --> jquery validation script imports
| |
| |- _ViewImports.cshtml --> shared imports and tag helpers for all views
| |- _ViewStart.cshtml --> shared values for all views
| |- ...
|
|-Controllers
| |-HomeController.cs
|
|- appsettings.json
|- Program.cs --> App entry-point
|- Startup.cs --> App config
```
**Note**: `_` prefix indicates page to be imported.
## Controllers
```cs
using Microsoft.AspNetCore.Mvc;
using App.Models;
using System.Collections.Generic;
namespace App.Controllers
{
public class CategoryController : Controller
{
private readonly AppDbContext _db;
// get db context through dependency injection
public CategoryController(AppDbContext db)
{
_db = db;
}
// GET /Controller/Index
public IActionResult Index()
{
IEnumerable<Entity> entities = _db.Entities;
return View(Entities); // pass data to the @model
}
// GET /Controller/Create
public IActionResult Create()
{
return View();
}
// POST /Controller/Create
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(Entity entity) // receive data from the @model
{
_db.Entities.Add(entity);
_db.SaveChanges();
return RedirectToAction("Index"); // redirection
}
// GET - /Controller/Edit
public IActionResult Edit(int? id)
{
if(id == null || id == 0)
{
return NotFound();
}
Entity entity = _db.Entities.Find(id);
if (entity == null)
{
return NotFound();
}
return View(entity); // return populated form for updating
}
// POST /Controller/Edit
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Edit(Entity entity)
{
if (ModelState.IsValid) // all rules in model have been met
{
_db.Entities.Update(entity);
_db.SaveChanges();
return RedirectToAction("Index");
}
return View(entity);
}
// GET /controller/Delete
public IActionResult Delete(int? id)
{
if (id == null || id == 0)
{
return NotFound();
}
Entity entity = _db.Entities.Find(id);
if (entity == null)
{
return NotFound();
}
return View(entity); // return populated form for confirmation
}
// POST /Controller/Delete
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Delete(Entity entity)
{
if (ModelState.IsValid) // all rules in model have been met
{
_db.Entities.Remove(entity);
_db.SaveChanges();
return RedirectToAction("Index");
}
return View(entity);
}
}
}
```
## Data Validation
### Model Annotations
In `Entity.cs`:
```cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
namespace App.Models
{
public class Entity
{
[DisplayName("Integer Number")]
[Required]
[Range(1, int.MaxValue, ErrorMessage = "Error Message")]
public int IntProp { get; set; }
}
}
```
### Tag Helpers & Client Side Validation
In `View.cshtml`;
```cs
<form method="post" asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group row">
<div class="col-4">
<label asp-for="IntProp"></label>
</div>
<div class="col-8">
<input asp-for="IntProp" class="form-control"/>
<span asp-validation-for="IntProp" class="text-danger"></span> // error message displayed here
</div>
</div>
</form>
// client side validation
@section Scripts{
@{ <partial name="_ValidationScriptsPartial" /> }
}
```
### Server Side Validation
```cs
using Microsoft.AspNetCore.Mvc;
using App.Models;
using System.Collections.Generic;
namespace App.Controllers
{
public class CategoryController : Controller
{
private readonly AppDbContext _db;
// get db context through dependency injection
public CategoryController(AppDbContext db)
{
_db = db;
}
// GET /Controller/Index
public IActionResult Index()
{
IEnumerable<Entity> entities = _db.Entities;
return View(Entities); // pass data to the @model
}
// GET /Controller/Create
public IActionResult Create()
{
return View();
}
// POST /Controller/Create
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(Entity entity) // receive data from the @model
{
if (ModelState.IsValid) // all rules in model have been met
{
_db.Entities.Add(entity);
_db.SaveChanges();
return RedirectToAction("Index");
}
return View(entity); // return model and display error messages
}
}
}
```

View file

@ -0,0 +1,236 @@
# Razor Pages
## Project Structure
```txt
Project
|-Properties
| |- launchSettings.json
|
|-wwwroot --> static files
| |-css
| | |- site.css
| |
| |-js
| | |- site.js
| |
| |-lib
| | |- jquery
| | |- bootstrap
| | |- ...
| |
| |- favicon.ico
|
|-Pages
| |-Shared
| | |- _Layout.cshtml --> reusable default page layout
| | |- _ValidationScriptsPartial --> jquery validation script imports
| |
| |- _ViewImports.cshtml --> shared imports and tag helpers for all views
| |- _ViewStart.cshtml --> shared values for all views
| |- Index.cshtml
| |- Index.cshtml.cs
| |- ...
|
|- appsettings.json --> application settings
|- Program.cs --> App entry-point
|- Startup.cs
```
**Note**: `_` prefix indicates page to be imported
Razor Pages components:
- Razor Page (UI/View - `.cshtml`)
- Page Model (Handlers - `.cshtml.cs`)
in `Index.cshtml`:
```cs
@page // mark as Razor Page
@model IndexModel // Link Page Model
@{
ViewData["Title"] = "Page Title" // same as <title>Page Title</title>
}
// body contents
```
in `Page.cshtml.cs`:
```cs
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
namespace App.Pages
{
public class IndexModel : PageModel
{
// HTTP Method
public void OnGet() { }
// HTTP Method
public void OnPost() { }
}
}
```
## Razor Page
```cs
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
namespace App.Pages
{
public class IndexModel : PageModel
{
private readonly ApplicationDbContext _db; // EF DB Context
// Get DBContext through DI
public IndexModel(ApplicationDbContext db)
{
_db = db;
}
[BindProperty] // assumed to be received on POST
public IEnumerable<Entity> Entities { get; set; }
// HTTP Method Handler
public async Task OnGet()
{
// get data from DB (example operation)
Entities = await _db.Entities.ToListAsync();
}
// HTTP Method Handler
public async Task<IActionResult> OnPost()
{
if (ModelState.IsValid)
{
// save to DB (example operation)
await _db.Entities.AddAsync(Entity);
await _db.SaveChangesAsync();
return RedirectToPage("Index");
}
else
{
return Page();
}
}
}
}
```
## Routing
Rules:
- URL maps to a physical file on disk
- Razor paged needs a root folder (Default "Pages")
- file extension not included in URL
- `Index.cshtml` is entry-point and default document (missing file in URL redirects to index)
| URL | Maps TO |
|------------------------|----------------------------------------------------|
| www.domain.com | /Pages/Index.cshtml |
| www.domain.com/Index | /Pages/Index.html |
| www.domain.com/Account | /Pages/Account.cshtml, /Pages/Account/Index.cshtml |
## Data Validation
### Model Annotations
In `Entity.cs`:
```cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
namespace App.Models
{
public class Entity
{
[DisplayName("Integer Number")]
[Required]
[Range(1, int.MaxValue, ErrorMessage = "Error Message")]
public int IntProp { get; set; }
}
}
```
### Tag Helpers & Client Side Validation
In `View.cshtml`;
```cs
<form method="post" asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group row">
<div class="col-4">
<label asp-for="IntProp"></label>
</div>
<div class="col-8">
<input asp-for="IntProp" class="form-control"/>
<span asp-validation-for="IntProp" class="text-danger"></span> // error message displayed here
</div>
</div>
</form>
// client side validation
@section Scripts{
@{ <partial name="_ValidationScriptsPartial" /> }
}
```
### Server Side Validation
```cs
using Microsoft.AspNetCore.Mvc;
using App.Models;
using System.Collections.Generic;
namespace App.Controllers
{
public class IndexModel : PageModel
{
private readonly ApplicationDbContext _db;
// get db context through dependency injection
public IndexModel(AppDbContext db)
{
_db = db;
}
[BindProperty]
public Entity Entity { get; set; }
public async Task OnGet(int id)
{
Entity = await _db.Entities.FindAsync(id);
}
public async Task<IActionResult> OnPost()
{
if (ModelState.IsValid)
{
await _db.SaveChangesAsync();
return RedirectToPage("Index");
}
else
{
return Page();
}
}
}
}
```

View file

@ -0,0 +1,164 @@
# [Razor Syntax](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/razor)
## Markup
```cs
@page // set this as razor page
@model <App>.Models.Entity // if MVC set type of elements passed to the view
@model <Page>Model // if Razor page set underlying class
@* razor comment *@
// substitute @variable with it's value
<tag>@variable</tag>
@{
// razor code block
// can contain C# or HTML
Model // access to passed @model (MVC)
}
@if (condition) { }
@for (init, condition, iteration) { }
@Model.Property // display Property value (MVC)
```
---
## Tag Helpers (ASP.NET Core)
**Tag helpers** are reusable components for automating the generation of HTML in Razor Pages. Tag helpers target specific HTML tags.
Example:
```html
<!-- tag helpers for a lin in ASP.NET MVC -->
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
```
### Managing Tag Helpers
The `@addTagHelper` directive makes Tag Helpers available to the view. Generally, the view file is `Pages/_ViewImports.cshtml`, which by default is inherited by all files in the `Pages` folder and subfolders, making Tag Helpers available.
```cs
@using <App>
@namespace <App>.Pages // or <Project>.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
```
The first parameter after `@addTagHelper` specifies the Tag Helpers to load (`*` for all Tag Helpers), and the second parameter (e.g. `Microsoft.AspNetCore.Mvc.TagHelpers`) specifies the assembly containing the Tag Helpers.
`Microsoft.AspNetCore.Mvc.TagHelpers` is the assembly for the built-in ASP.NET Core Tag Helpers.
#### Opting out of individual elements
It's possible to disable a Tag Helper at the element level with the Tag Helper opt-out character (`!`)
```cshtml
<!-- disable email validation -->
<!span asp-validation-for="Email" ></!span>
```
### Explicit Tag Helpers
The `@tagHelperPrefix` directive allows to specify a tag prefix string to enable Tag Helper support and to make Tag Helper usage explicit.
```cshtml
@tagHelpersPrefix th:
```
### Important Tag Helpers (`asp-`) & HTML Helpers (`@Html`)
[Understanding Html Helpers](https://stephenwalther.com/archive/2009/03/03/chapter-6-understanding-html-helpers)
```cs
@model <App>.Models.Entity
// Display the name of the property
@Html.DisplayNameFor(model => model.EntityProp)
@nameof(Model.EntityProp)
// Display the value of the property
@Html.DisplayFor(model => model.EntityProp)
@Model.EntityProp
<from>
// use the property as the label, eventually w/ [DisplayName("...")]
<label asp-for="EntityProp"></label>
@Html.LabelFor()
// automatically set the value at form compilation and submission
<input asp-for="EntityProp"/>
@Html.EditorFor()
</from>
// route config is {Controller}/{Action}/{Id?}
<a asp-controller="<Controller>" asp-action="<Action>">Link</a> // link to /Controller/Action
<a asp-controller="<Controller>" asp-action="<Action>" asp-route-Id="@model.Id">Link</a> // link to /Controller/Action/Id
@Html.ActionLink("<Link Text>", "<Action>", "<Controller>", new { @HTmlAttribute = value, Id = value }) // link to /Controller/Action/Id
// link to /Controller/Action?queryParameter=value
@Html.ActionLink("<Link Text>", "<Action>", "<Controller>", new { @HTmlAttribute = value, queryParameter = value })
<a asp-controller="<Controller>" asp-action="<Action>" asp-route-queryParameter="value">Link</a> // asp-route-* for query strings
```
### [Select Tag Helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/working-with-forms)
[StackOverflow](https://stackoverflow.com/a/34624217)
[SelectList Docs](https://docs.microsoft.com/en-us/dotnet/api/system.web.mvc.selectlist)
In `ViewModel.cs`:
```cs
class ViewModel
{
public int EntityId { get; set; } // value selected in form ends up here
// object has numeric id and other props
public SelectList Entities = new()
public ViewModel(){ } // parameterless constructor (NEEDED)
}
```
In `View.cs`
```cs
@model ViewModel
<form asp-controller="Controller" asp-action="PostAction">
<select asp-for"EntityId" asp-items="Model.Entities">
</select>
<button type="submit">Send<button>
</form>
```
In `Controller.cs`:
```cs
public IActionResult GetAction()
{
var vm = new ViewModel();
vm.Entities = new SelectList(_context.Entities, "Id", "Text"); // fill SelectList
vm.EntityId = value; // set selected option (OPTIONAL)
return View(vm);
}
[HttpPost]
public IActionResult PostAction(ViewModel)
{
if(ModelState.IsValid)
{
// extract info from view model
// save to db
}
}
```

View file

@ -0,0 +1,53 @@
# ASP.NET REST API
```cs
[Route("api/endpoint")]
[ApiController]
public class EntitiesController : ControllerBase // API controller
{
private readonly IEntityService _service;
public EntitiesController(IEntityService service, IMapper mapper)
{
_service = service;
_mapper = mapper
}
[HttpGet] // GET api/endpoint
public ActionResult<IEnumerable<EntityDTO>> GetEntities()
{
IEnumerable<EntityDTO> results = /* ... */
return Ok(results);
}
[HttpGet("{id}")] // GET api/endpoint/{id}
public ActionResult<EntityDTO> GetEntityById(int id)
{
var result = /* .. */;
if(result != null)
{
return Ok(result);
}
return NotFound();
}
[HttpPost] // POST api/endpoint
public ActionResult<EntityDTO> CreateEntity([FromBody] EntityDTO entity)
{
// persist the entity
var id = /* ID of the created entity */
return Created(id, entity);
}
[HttpPut] // PUT api/endpoint
public ActionResult<EntityDTO> UpdateEntity([FromBody] EntityDTO entity)
{
// persist the updated entity
return Created(uri, entity);
}
}
```

View file

@ -0,0 +1,201 @@
# SignalR
The SignalR Hubs API enables to call methods on connected clients from the server. In the server code, define methods that are called by client. In the client code, define methods that are called from the server. SignalR takes care of everything behind the scenes that makes real-time client-to-server and server-to-client communications possible.
## Server-Side
### Configuration
In `Startup.cs`:
```cs
namespace App
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the DI container.
public void ConfigureServices(IServiceCollection services)
{
services.AddSignalR();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseEndpoints(endpoints =>
{
endpoints.MapHub("/hub/endpoint");
});
}
}
}
```
### Creating Hubs
```cs
public class CustomHub : Hub
{
public task HubMethod(Type args)
{
// trigger function on all clients and pass args to it
return Clients.All.SendAsync("CLientMethod", args);
// trigger function on caller client and pass args to it
return Clients.Caller.SendAsync("CLientMethod", args);
// trigger function on clients of a group and pass args to it
return Clients.Group("GroupName").SendAsync("CLientMethod", args);
// other operations
}
}
```
### Strongly Typed Hubs
A drawback of using `SendAsync` is that it relies on a magic string to specify the client method to be called. This leaves code open to runtime errors if the method name is misspelled or missing from the client.
An alternative to using SendAsync is to strongly type the Hub with `Hub<T>`.
```cs
public interface IHubClient
{
// matches method to be called on the client
Task ClientMethod(Type args);
}
```
```cs
public class CustomHub : Hub<IHubClient>
{
public Task HubMethod(Type args)
{
return Clients.All.ClientMethod(args);
}
}
```
Using `Hub<T>` enables compile-time checking of the client methods. This prevents issues caused by using magic strings, since `Hub<T>` can only provide access to the methods defined in the interface.
Using a strongly typed `Hub<T>` disables the ability to use `SendAsync`. Any methods defined on the interface can still be defined as asynchronous. In fact, each of these methods should return a `Task`. Since it's an interface, don't use the `async` keyword.
### Handling Connection Events
The SignalR Hubs API provides the OnConnectedAsync and OnDisconnectedAsync virtual methods to manage and track connections. Override the OnConnectedAsync virtual method to perform actions when a client connects to the Hub, such as adding it to a group.
```cs
public override async Task OnConnectedAsync()
{
await Groups.AddToGroupAsync(Context.ConnectionId, "GroupName");
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, "GroupName");
await base.OnDisconnectedAsync(exception);
}
```
Override the `OnDisconnectedAsync` virtual method to perform actions when a client disconnects.
If the client disconnects intentionally (by calling `connection.stop()`, for example), the exception parameter will be null.
However, if the client is disconnected due to an error (such as a network failure), the exception parameter will contain an exception describing the failure.
### Sending Errors to the client
Exceptions thrown in the hub methods are sent to the client that invoked the method. On the JavaScript client, the `invoke` method returns a JavaScript Promise. When the client receives an error with a handler attached to the promise using catch, it's invoked and passed as a JavaScript `Error` object.
If the Hub throws an exception, connections aren't closed. By default, SignalR returns a generic error message to the client.
If you have an exceptional condition you *do* want to propagate to the client, use the `HubException` class. If you throw a `HubException` from your hub method, SignalR will send the entire message to the client, unmodified.
```cs
public Task ThrowException()
{
throw new HubException("This error will be sent to the client!");
}
```
### Client-Side (JavaScript)
### Installing the client package
```sh
npm init -y
npm install @microsoft/signalr
```
npm installs the package contents in the `node_modules\@microsoft\signalr\dist\browser` folder. Create a new folder named signalr under the `wwwroot\lib` folder. Copy the signalr.js file to the `wwwroot\lib\signalr` folder.
Reference the SignalR JavaScript client in the `<script>` element. For example:
```html
<script src="~/lib/signalr/signalr.js"></script>
```
### Connecting to a Hub
[Reconnect Clients Docs](https://docs.microsoft.com/en-us/aspnet/core/signalr/javascript-client#reconnect-clients)
```js
const connection = new signalR.HubConnectionBuilder()
.withUrl("/hub/endpoint")
.configureLogging(signalR.LogLevel.Information)
.withAutomaticReconnect() // optional
.build();
// async/await connection start
async function connect() {
try {
await connection.start();
console.log("SignalR Connected.");
} catch (err) {
console.error(err);
}
};
// promise connection start
function connect() {
connection.start()
.then(() => {})
.catch((err) => {console.error(err)});
}
```
### Call hub methods fom the client
JavaScript clients call public methods on hubs via the `invoke` method of the `HubConnection`. The `invoke` method accepts:
- The name of the hub method.
- Any arguments defined in the hub method.
```js
try {
await connection.invoke("HubMethod", args);
} catch (err) {
console.error(err);
}
```
The `invoke` method returns a JavaScript `Promise`. The `Promise` is resolved with the return value (if any) when the method on the server returns. If the method on the server throws an error, the `Promise` is rejected with the error message. Use `async` and `await` or the `Promise`'s then and catch methods to handle these cases.
JavaScript clients can also call public methods on hubs via the the `send` method of the `HubConnection`. Unlike the `invoke` method, the send method doesn't wait for a response from the server. The send method returns a JavaScript `Promise`. The `Promise` is resolved when the message has been sent to the server. If there is an error sending the message, the `Promise` is rejected with the error message. Use `async` and `await` or the `Promise`'s then and catch methods to handle these cases.
### Call client methods from the hub
To receive messages from the hub, define a method using the `on` method of the `HubConnection`. The `on` method accepts:
- The name of the JavaScript client method.
- Arguments the hub passes to the method.
```cs
connection.on("ClientMethod", (args) => { /* ... */});
```

View file

@ -0,0 +1,85 @@
# WebForms
## `Page.aspx`
The fist loaded page is `Default.aspx` and its underlying code.
```html
<!-- directive -->
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="Project.Default" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"> <!-- XML Namespace -->
<head runat="server"> <!-- runat: handle as ASP code -->
<title></title>
</head>
<body>
<!-- web forms require a form tag to be the whole body -->
<form id="form1" runat="server"> <!-- runat: handle as ASP code -->
<div>
</div>
</form>
</body>
</html>
```
### Page Directive
```cs
<%@ Page Language="C#" // define language used (can be C# or VB)
AutoEventWireup="true" // automatically create and setup event handlers
CodeBehind="Default.aspx.cs" // define the underlying code file
Inherits="EmptyWebForm.Default" %>
```
### Web Controls
```xml
<asp:Control ID="" runat="server" ...></asp:Control>
<!-- Label: empty text will diplay ID, use empty space as text for empty label -->
<asp:Label ID="lbl_" runat="server" Text=" "></asp:Label>
<!-- TextBox -->
<asp:TextBox ID="txt_" runat="server"></asp:TextBox>
<!-- Button -->
<asp:Button ID="btn_" runat="server" Text="ButtonText" OnClick="btn_Click" />
<!-- HyperLink -->
<asp:HyperLink ID="lnk_" runat="server" NavigateUrl="~/Page.aspx">LINK TEXT</asp:HyperLink>
<!-- LinkButton: POstBackEvent reloads the page -->
<asp:LinkButton ID="lbtHome" runat="server" PostBackUrl="~/Page.aspx" OnClick="lbt_Click">BUTTON TEXT</asp:LinkButton>
<!-- Image -->
<asp:Image ID="img_" runat="server" ImageUrl="~/Images/image.png"/>
<!-- ImageButton -->
<asp:ImageButton ID="imb_" runat="server" ImageUrl="~/Images/image.png" PostBackUrl="~/Page.aspx"/>
<!-- SqlSataSource; connection string specified in Web.config -->
<asp:SqlDataSource ID="sds_" runat="server" ConnectionString="<%$ ConnectionStrings:ConnectionString %>" SelectCommand="SQL Query"></asp:SqlDataSource>
```
## `Page.aspx.cs`
```cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace Project
{
public partial class Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
}
protected void Control_Event(object sender, EventArgs e)
{
// actions on event trigger
}
}
}
```