mirror of
https://github.com/m-lamonaca/dev-notes.git
synced 2025-04-05 18:36:41 +00:00
feat(dotnet): Add .NET 7 notes (#9)
- add minimal api filter notes - add new auth setup notes - add `LibraryImport` source generator notes - add nav manager & get/set/after binding modifiers notes - add `file` access modifier notes - add minimal api output cache notes - add static lambda annotation notes
This commit is contained in:
parent
bf0a55622f
commit
27006a7108
3 changed files with 185 additions and 5 deletions
|
@ -1341,6 +1341,7 @@ protected // visible only to the same class and extending classes
|
|||
internal // available for use only within the assembly (default for classes)
|
||||
protected internal // can be accessed by any code in the assembly in which it's declared, or from within a derived class in another assembly
|
||||
private protected // can be accessed only within its declaring assembly, by code in the same class or in a type that is derived from that class
|
||||
file // can be accessed only in the same file in which is declared. Will not clash with other classes/method/interface with the same name
|
||||
|
||||
readonly // value can be read but not modified
|
||||
static // not of the instance of a class
|
||||
|
@ -1437,10 +1438,7 @@ class Class
|
|||
set => _backingField = value;
|
||||
}
|
||||
|
||||
// access backing field with the field keyword [C# 11?]
|
||||
public Type Property { get => field; set => field = value; }
|
||||
|
||||
// required property [C# 11?], prop must be set at obj init (in constructor or initializer)
|
||||
// REQUIRED PROPERTY prop must be set at obj init (in constructor or initializer)
|
||||
public required Type Property { get; set; }
|
||||
|
||||
// EXPRESSION-BODIED READ-ONLY PROPERTY
|
||||
|
@ -2913,7 +2911,7 @@ unsafe
|
|||
}
|
||||
```
|
||||
|
||||
### DllImport & Extern
|
||||
### External Code
|
||||
|
||||
The `extern` modifier is used to declare a method that is implemented externally.
|
||||
A common use of the extern modifier is with the `DllImport` attribute when using Interop services to call into _unmanaged_ code.
|
||||
|
|
|
@ -261,6 +261,16 @@ Project
|
|||
|
||||
## State Management
|
||||
|
||||
## Passing state with `NavigationManager`
|
||||
|
||||
It's now possible to pass state when navigating in Blazor apps using the `NavigationManager`.
|
||||
|
||||
```cs
|
||||
navigationManager.NavigateTo("/<route>", new NavigationOptions { HistoryEntryState = value });
|
||||
```
|
||||
|
||||
This mechanism allows for simple communication between different pages. The specified state is pushed onto the browser’s history stack so that it can be accessed later using either the `NavigationManager.HistoryEntryState` property or the `LocationChangedEventArgs.HistoryEntryState` property when listening for location changed events.
|
||||
|
||||
### Blazor WASM
|
||||
|
||||
```cs
|
||||
|
@ -355,6 +365,9 @@ public class StateContainer
|
|||
|
||||
<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
|
||||
|
||||
<input @bind="{PROPERTY}" @bind:after="{DELEGATE}" /> // run async logic after bind event completion
|
||||
<input @bind:get="{PROPERTY}" @bind:set="PropertyChanged" /> // two-way data binding
|
||||
</p>
|
||||
|
||||
@code {
|
||||
|
@ -372,11 +385,17 @@ public class StateContainer
|
|||
await PropertyChanged.InvokeAsync(e, argument); // notify parent bound prop has changed
|
||||
await elementReference.FocusAsync(); // focus an element in code
|
||||
}
|
||||
|
||||
[Parameter] public TValue Value { get; set; }
|
||||
[Parameter] public EventCallback<TValue> ValueChanged { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
> **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.
|
||||
|
||||
> **Note**: The `@bind:get` and `@bind:set` modifiers are always used together.
|
||||
> `The @bind:get` modifier specifies the value to bind to and the `@bind:set` modifier specifies a callback that is called when the value changes
|
||||
|
||||
## Javascript/.NET Interop
|
||||
|
||||
[Call Javascript from .NET](https://docs.microsoft.com/en-us/aspnet/core/blazor/call-javascript-from-dotnet)
|
||||
|
|
|
@ -84,6 +84,80 @@ 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:
|
||||
|
@ -100,6 +174,21 @@ app.MapGet("/hello", (ClaimsPrincipal user) => {
|
|||
});
|
||||
```
|
||||
|
||||
## OpenAPI
|
||||
|
||||
The `Microsoft.AspNetCore.OpenApi` package exposes a `WithOpenApi` extension method that generates an `OpenApiOperation` derived from a given endpoint’s 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.
|
||||
|
@ -150,6 +239,8 @@ builder.Services.AddAuthorization(options =>
|
|||
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
|
||||
.RequireAuthenticatedUser();
|
||||
})
|
||||
// or
|
||||
builder.Authentication.AddJwtBearer(); // will automatically add required middlewares
|
||||
|
||||
// [...]
|
||||
|
||||
|
@ -160,6 +251,78 @@ app.UseAuthorization(); // must come before routes
|
|||
|
||||
app.MapGet("/alcohol", () => Results.Ok()).RequireAuthorization("<policy>"); // on specific endpoints
|
||||
app.MapGet("/free-for-all", () => Results.Ok()).AllowAnonymous();
|
||||
app.MapGet("/special-secret", () => "This is a special secret!")
|
||||
.RequireAuthorization(p => p.RequireClaim("scope", "myapi:secrets"));
|
||||
```
|
||||
|
||||
### Local JWT Tokens
|
||||
|
||||
The `user-jwts` tool is similar in concept to the existing `user-secrets` tools, in that it can be used to manage values for the app that are only valid for the current user (the developer) on the current machine.
|
||||
In fact, the `user-jwts` tool utilizes the `user-secrets` infrastructure to manage the key that the JWTs will be signed with, ensuring it’s stored safely in the user profile.
|
||||
|
||||
```sh
|
||||
dotnet user-jwts create # configure a dev JWT fot the current user
|
||||
```
|
||||
|
||||
## Output Caching
|
||||
|
||||
```cs
|
||||
builder.Services.AddOutputCaching(); // no special options
|
||||
builder.Services.AddOutputCaching(options =>
|
||||
{
|
||||
options => options.AddBasePolicy(x => x.NoCache()) // no cache policy
|
||||
|
||||
Func<OutputCacheContext, bool> predicate = /* discriminate requests */
|
||||
options.AddBasePolicy(x => x.With(predicate).CachePolicy());
|
||||
options.AddBasePolicy("<policy-name>", x => x.CachePolicy()); // named policy
|
||||
});
|
||||
|
||||
// [...]
|
||||
|
||||
app.UseOutputCaching(); // following middlewares can use output cache
|
||||
|
||||
// [...]
|
||||
|
||||
app.MapGet("/<route>", RouteHandler).CacheOutput(); // cache forever
|
||||
app.MapGet("/<route>", RouteHandler).CacheOutput().Expire(timespan);
|
||||
|
||||
app.MapGet("/<route>", RouteHandler).CacheOutput(x => x.CachePolicy());
|
||||
app.MapGet("/<route>", RouteHandler).CacheOutput("<policy-name>");
|
||||
|
||||
app.MapGet("/<route>", RouteHandler).CacheOutput(x => x.VaryByHeader(/* headers list */));
|
||||
app.MapGet("/<route>", RouteHandler).CacheOutput(x => x.VaryByQuery(/* query key */));
|
||||
app.MapGet("/<route>", RouteHandler).CacheOutput(x => x.VaryByValue());
|
||||
|
||||
app.MapGet("/<route>", [OutputCache(/* options */)]RouteHandler);
|
||||
```
|
||||
|
||||
### Cache Eviction
|
||||
|
||||
```cs
|
||||
|
||||
app.MapGet("/<route-one>", RouteHandler).CacheOutput(x => x.Tag("<tag>")); // tag cache portion
|
||||
|
||||
app.MapGet("/<route-two>", (IOutputCacheStore cache, CancellationToken token) =>
|
||||
{
|
||||
await cache.EvictByTag("<tag>", token); // invalidate a portion of the cache
|
||||
});
|
||||
```
|
||||
|
||||
### Custom Cache Policy
|
||||
|
||||
```cs
|
||||
app.MapGet("/<route-one>", RouteHandler).CacheOutput(x => x.AddCachePolicy<CustomCachePolicy>());
|
||||
```
|
||||
|
||||
```cs
|
||||
class CustomCachePolicy : IOutputCachePolicy
|
||||
{
|
||||
public ValueTask CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) { }
|
||||
|
||||
public ValueTask ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) { }
|
||||
|
||||
public ValueTask ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) { }
|
||||
}
|
||||
```
|
||||
|
||||
## Output Caching
|
||||
|
|
Loading…
Add table
Reference in a new issue