 
The CSRF attack happens when an active session has been established with some website and when a maliciouis site tries to submit bad data on top of it.
The session identifier is stored in a cookie which is automatically being submitted on every HTTP request.
In order to overcome this issue we can mark the session cookie as "Strict" or send some extra data as a cookie and expect it back as a header. The malicious site may submit the cookie but it cannot read it's value.
Here are the basic steps to set up a CSRF protection in ASP.NET core.
Add the anti forgery service to the startup code:
builder.Services.AddAntiforgery((options) => {
    options.HeaderName = "X-XSRF-TOKEN";
    options.SuppressXFrameOptionsHeader = false;
});
Inject the "IAntiForgery" service in a controller that is called immediately after login
Set the anti forgery cookie in an action called immediately after login:
var tokenSet = antiforgery.GetAndStoreTokens(HttpContext);
Response.Cookies.Append(
    "XSRF-TOKEN",
    tokenSet.RequestToken!,
    new CookieOptions {
        HttpOnly = false
   });
or in a middleware, right after AddAuthentication or else the token won't include the current identity
app.Use(async (context, next) =>
{
    using var scope = context.RequestServices.CreateScope();
    var antiforgery = scope.ServiceProvider.GetRequiredService<IAntiforgery>();
    if (context.User.Identity != null)
    {
        var tokens = antiforgery.GetAndStoreTokens(context);
        context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken!,
            new CookieOptions
            {
                HttpOnly = false,
                MaxAge = TimeSpan.FromMinutes(5),
            });
    }
    
    await next();
});
If you name the cookie "XSRF-TOKEN" it will be picked up automatically by various libraries like Axios or Angular.
To validate the anti forgery token DO NOT use [ValidateAntiForgeryToken] attribute, instead check it with the IAntiforgery service:
bool requestValid = await _antiforgery.IsRequestValidAsync(HttpContext);
if (!requestValid)
    return BadRequest();
Note, this skips safe HTTP verbs (GET/OPTIONS)
Create a custom action filter with it:
public class AntiForgeryActionFilterAttribute: ActionFilterAttribute
{
        public override async Task OnActionExecutionAsync(ActionExecutingContext context, 
        	ActionExecutionDelegate next)
        {
            using var scope = context.HttpContext.RequestServices.CreateScope();
            var antiforgery = scope.ServiceProvider.GetService<IAntiforgery>();
            bool requestValid = await antiforgery.IsRequestValidAsync(context.HttpContext);
            if (!requestValid)
            {
                context.Result = new BadRequestResult();
            }
            else
            {
                await next();
            }
        }
}