• Featured post

C# Task async programming (TAP) and parallel code

The core for asynchronous programming are the objects Task and Task<T>. Both of them are compatible with the keywords async and await.

First of all we need to identify if the code’s I/O-bound or CPU-bound.

  • the code’s limited for external operations and waits for something a lot of time. Examples of this are DDBB calls, or a server’s response. In this case we have to use async/await to free the thread while we wait
  • the code does a CPU-intensive operation. Then we move the work to another thread using Task.Run() so we don’t block the main thread.

async code vs parallel code

(!) Asynchronous code is not the same as parallel code (!)

  • In async code you are trying to make your threads do as little work as possible. This will keep your app responsibe, capable to serve many requests at once and scale well.
  • In parallel code you do the opposite. You use and keep a hold on a thread to do CPU-intensive calculations

async code

The importante of async programming is that you choose when to wait on a task. This way, you can start other tasks concurrently

In async code, one single thread can start the next task concurrently before the previous one completes.
(!) async code doesn’t cause additional threads to be created because an async method doesn’t run on its own thread. (!) It runs on the current synchronization context and uses time on the thread only when the method is active.

parallel code

For parallelism you need multiple threads where each thread executes a task, and all of those tasks are executed at the same time

Read More

C# User Secrets

Never store passwords or sensitive data in source code or configuration files. Production secrets shouldn’t be used for development or test. Secrets shouldn’t be deployed with the app. Production secrets should be accessed through a controlled means like Azure Key Vault.

Secret manager

This tool hides implementation details. The secret values are stored in a JSON file in the local machine’s user profile folder.

This tool operates on project-specific configuration settings and (!) it’s only meant for local development (!). Don’t use it for production as it’s not encrypted.

To use user secrets, run the following command in the project directory

dotnet user-secrets init

You can do this through visual studio Right click on your project inside vstudio > Administrar secretos de usuario

Set a new secret

Define an app secret containing a key > value

dotnet user-secrets set "OpenAI:ApiKey" "sk-xxxx"

Read a secret

var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddUserSecrets<Program>();
string openAiKey = builder.Configuration["OpenAI:ApiKey"];

Read a secret - using DI

public class IndexModel(IConfiguration _config) : PageModel
{
	public void OnGet()
	{
		var openAiKey = _config["OpenAI:ApiKey"];
	}
}

Delete a secret

dotnet user-secrets remove "OpenAI:ApiKey"

Reference(s)

https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-9.0&tabs=windows

.NET AI integration

Today’s AI landscape moves so fast and providers differ so much that vendor lock-in can become expensive. You need a clean, testeable way to add AI without tying your architecture to one SDK.

The solution to this problem is a model-agnostic solution.

Nuggets to use (you need to click to see preliminar versions):

  • Microsoft.Extensions.AI - This nugget implements IChatClient interface, which is an abstraction to use several LLM providers, from ChatGPT to Ollama.
  • Microsoft.Extensions.AI.OpenAI
  • OllamaSharp (previously Microsoft.Extensions.AI.Ollama)

You’ll need to go to https://platform.openai.com/ to set up a project, billing, and get an openAI API key.

This repository is a test implementation which connects to OpenAi’s ChatGPT and is able to send prompts.

Best Practices

  • Keep inputs short and specific
  • Validate outputs with regex/JSON schema. Reject or re-ask when invalid
  • Log prompts, token counts, latency and provider responses
  • Improve cost ops. Cache results, batch requests and prefer smaller models by default
  • Don’t commit or send secrets or personal information
  • Failover. Implement timeouts, retries, and fallback models
  • LLMs are stateless; maintaining and reconstructing conversational context is a developer’s responsibility (chat history or memory abstractions)

Security

  • prompt injection: beware with malicious prompts to subvert model guardrails, steal data or execute unintended actions
  • LLMs may leak private or internal data via crafted prompts
  • Training data poisoning may be injected by malicious actors
  • DoS and rate limiting: prevent overuse / abuse

Reference(s)

https://roxeem.com/2025/09/04/the-practical-net-guide-to-ai-llm-introduction/
https://roxeem.com/2025/09/08/how-to-correctly-build-ai-features-in-dotnet/

EF Core multithreading

I’ve had issues with EF Core when operating with multiple threads and with multiple calls at the same time.

The most important things to check are:

  1. The DbContext is not being shared between calls or threads
  2. All classes which have the context inyected must be scoped (not singleton)
  3. If working with async methods, you need to await calls

I have the following service

public class PersonService(AppDbContext _context)
{
	public async Task<Person> GetPerson(string id)
	{
		return await context.Persons.Find(id);
	}
}

which I may configure as follows

// if I inject it as singleton, this would cause exceptions on multiple calls
services.AddSingleton<IPersonService, PersonService>

// we have to inject it as scoped so it creates a context new for each call
services.AddScoped<IPersonService, PersonService>

Caching in .NET (`IMemoryCache`)

.NET offers several cache types. I’m going to explore here IMemoryCache which stores data in the memory of the web server. It’s simple but not suitable for distributed scenarios.

first of all we need to register the services

builder.Services.AddMemoryCache();

GetOrCreateAsync

here’s how you can inject and use it, without manipulating the cache itself

public class PersonService(IMemoryCache _cache)
{
	private const string CACHE_PERSON_KEY = "PersonService:GetPerson:";

	public async Task<Person> GetPerson(string id)
	{
		return await _cache.GetOrCreateAsync(CACHE_PERSON_KEY + id, async entry =>
		{
			entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
			return await GetPersonNoCache(id);
		});
	}

	public async Task<Person> GetPersonNoCache(string id)
	{
		// do operations to get a person here
	}
}

TryGetValue

this is a more manual approach.

public class PersonService(AppDbContext _context, IMemoryCache _cache)
{
	private const string CACHE_PERSON_KEY = "PersonService:GetPerson:";

	public async Task<Person> GetPerson(string id)
	{
		if(!_cache.TryGetValue(CACHE_PERSON_KEY+id, out Person person))
		{
			person = await context.Persons.Find(id);

			var cacheEntryOptions = new MemoryCacheEntryOptions()
				.SetAbsoluteExpiration(TimeSpan.FromMinutes(10))
				.SetSlidingExpiration(TimeSpan.FromMinutes(2));

			cache.Set(CACHE_PERSON_KEY+id, person, cacheEntryOptions);
		}
		return person;
	}
}

Reference(s)

https://www.milanjovanovic.tech/blog/caching-in-aspnetcore-improving-application-performance

Three-point estimation

Split tasks in its minimum definition and estimate those minimum tasks by Optimistic (O) - Most Likely (M) - Pessimistic (P).

With those estimations we do PERT distribution and then add those estimations

(O+(4xM)+P)/6

Example

Task: Migrate x database Minimum tasks:

  • migrate service 1 to y database
  • migrate service 2 to y database
  • migrate connector to use y database
  • test changes in test env

Then we estimate those tasks

task Optimistic Most likely Pessimistic PERT Comments
migrate service 1 10h 25h 55h 28h (I round hours up)
migrate service 2 4h 14h 22h 14h take x into account
migrate connector 20h 40h 80h 44h  
test changes 2h 7h 14h 8h  
total estimation for task       94h  

Reference(s)

https://www.knowledgehut.com/blog/project-management/three-point-estimating

C# TAP programming inside iterations

The following is an example where we need to call and await an external API multiple times inside an iteration.

I’m using myFakeAPI from postman for this example and one of their Car response look like this

public class CarResponse
{
	public CarDto Car { get; set; }
}

public class CarDto
{
	public int Id { get; set; }
	public string Car { get; set; }
	public string Car_Model { get; set; }
	public string Car_Color { get; set; }
	public int Car_Model_Year { get; set; }
	public string Car_Vin { get; set; }
	public string Price { get; set; }
	public bool Availability { get; set; }
}

Then this is the method which does call and mapping

private async Task<CarResponse> ExecuteCall(string id)
{
	string combinedUrl = URL + id;

	using var response = await _httpClient.GetAsync(combinedUrl);
	response.EnsureSuccessStatusCode();

	string json = await response.Content.ReadAsStringAsync();
	return JsonConvert.DeserializeObject<CarResponse>(json);
}

Control

This is the control version where we launch and await the tasks one at a time

// DON'T DO THIS
private async Task<List<CarResponse>> Control()
{
	List<CarResponse> carList = [];
	foreach (string id in _idsList)
	{
		CarResponse singleCar = await ExecuteCall(id);
		carList.Add(singleCar);
	}
	return carList;
}

Task.WhenAll()

It’s the most simple one - all tasks are launched at the same time. It’s ideal when we don’t have limits as we have no control over the simultaneous number of calls

// simple but what if we'd have +100 calls?
private async Task<List<CarResponse>> TaskWhenAll()
{
	var getCarsTask = _idsList.Select(ExecuteCall);
	var cars = await Task.WhenAll(getCarsTask);
	return cars.ToList();
}

Parallel.ForEachAsync()

(.NET6+) this gives us the most control over number of parallel calls. It’s more complex.

private async Task<List<CarResponse>> ParallelForEachAsync()
{
	// this is a secure collection for multiple threads
	var carsBag = new ConcurrentBag<CarResponse>();
	var options = new ParallelOptions { MaxDegreeOfParallelism = 5 };

	await Parallel.ForEachAsync(_idsList, options, async (id, ct) =>
	{
		CarResponse car = await ExecuteCall(id);
		carsBag.Add(car);
	});
	return carsBag.ToList();
}

Results

For 50 calls:

  • control took 19s
  • Task.WhenAll() took 0.9s
  • Parallel.ForEachAsync with a degree of parallelism of 10, took 1.5s

As we see both of them are a great improve over control.

C# generics

Example on how to use generics in C#

public class AnimalService(IConnectorService _service)
{
	public async Task<List<T>> GetAnimals<T> (List<string> ids, string query)
	{
		List<T> results = [];
		var request = new ConnectorRequest
		{
			query = query,
			ids = ids
		};
		response = await _service.Execute(request);
		if((response?.result?.Count ?? 0) > 0)
		{
			results = JsonConvert.DeserializeObject<List<T>>(response.result);
		}
		return results;
	}
}

C# JSON tags Newtonsoft

JsonConvert.SerializeObject

I use this to serialize full objects to log them with all their properties

InputModel x = // ...
log.LogInfo($"doing x. input: {JsonConvert.SerializeObject(x)}");

JsonProperty and NullValueHandling

This is useful for cases where we need to modify the given properties of a class we serialize and give back, but for any reason we don’t want to change the internal structure or naming.

With NullValueHandling we may omit in the JSON a variable in case it’s null.

public class House
{
	public List<Window> windows { get; set; };
	
	[JsonProperty("builtInGarage"), NullValueHandling = NullValueHandling.Ignore]
	public Garage garage { get; set; }; 
}

C# How to get headers

This is how to retrieve headers from any call.

if(Request.Headers.TryGetValue("mandatory-header", out var mandatoryHeader))
{
	// may be either filled or empty string
	string optionalHeader = Request.Headers["optional-header"];
	var result = await _service.DoWork(mandatoryHeader, optionalHeader)
}
else 
{
	// log error as mandatory-header isn't included in the call
}

C# Async exceptions

Tasks throw exceptions when they can’t complete successfully. The client code catches those exceptions when the await expression is applied to a started task.

When a task that runs async throws an exception, that task is faulted. The Task object holds the exception thrown. Faulted tasks throw an exception when the await expression is applied to the task.