Introduction
In our last two posts, we compared F# and C# side-by-side in both theory and implementation. Today, let’s explore a practical use case that’s increasingly common in modern systems: using F# for business logic and C# for infrastructure, especially in a microservice environment.
This hybrid approach allows teams to get the best of both worlds:
- F#’s clarity and correctness for domain modeling and core computation
- C#’s ecosystem maturity and tooling for web APIs, DI, and integration
Let’s break it down with examples, structure, and a real-world integration plan.
📐 Why Mix F# and C#?
Here’s a typical scenario:
| Layer | F# Benefits | C# Benefits |
|---|---|---|
| Domain Logic | Discriminated unions, immutability, safety | Verbose, but doable |
| API Layer | Can work, but tooling weaker | ASP.NET Core, Swagger, filters, attributes |
| Cloud Integrations | Works via libraries | Mature SDKs, documentation, Azure Functions |
| Testing/Tooling | FsUnit, Expecto | xUnit, NUnit, excellent IDE support |
So why not define your domain and computation logic in F#, and host and expose it via C#?
🏗️ Project Structure
A clean structure looks like this:
/src
/Shared.Domain (F#)
- PaymentTypes.fs
- PricingLogic.fs
/Web.Api (C#)
- Program.cs
- Controllers/
/Tests
/Domain.Tests (F#)
/Api.Tests (C#)
Shared.Domainis F#, containing pure functions, types, and domain logicWeb.Apiis C#, referencing the F# project and exposing REST endpointsTestsare split to match language domains
🧪 Sample Domain in F# (Shared.Domain)
namespace Shared.Domain
type PaymentMethod =
| Cash
| CreditCard of string
| PayPal of string
module PaymentProcessor =
let describe payment =
match payment with
| Cash -> "Cash payment"
| CreditCard n -> $"Card ending in {n.Substring(n.Length - 4)}"
| PayPal email -> $"PayPal: {email}"
- This module is clean, testable, and doesn’t depend on ASP.NET or I/O.
- You can easily write tests using Expecto or FsUnit.
🌐 Exposing F# Logic in ASP.NET (C# Web.Api)
In your Web.Api.csproj:
<ItemGroup>
<ProjectReference Include="..\Shared.Domain\Shared.Domain.fsproj" />
</ItemGroup>
C# Controller Using F# Logic
using Microsoft.AspNetCore.Mvc;
using Shared.Domain;
[ApiController]
[Route("api/[controller]")]
public class PaymentController : ControllerBase
{
[HttpGet("describe")]
public IActionResult Describe([FromQuery] string method, [FromQuery] string? value = null)
{
PaymentMethod payment = method.ToLower() switch
{
"cash" => new PaymentMethod.Cash(),
"creditcard" when value is not null => new PaymentMethod.CreditCard(value),
"paypal" when value is not null => new PaymentMethod.PayPal(value),
_ => throw new ArgumentException("Invalid input")
};
string description = PaymentProcessor.describe(payment);
return Ok(description);
}
}
- You construct the F# discriminated union in C# using the compiled types.
- The logic remains purely functional and testable in F#, while the API surface is idiomatic ASP.NET.
🔍 Common Gotchas and Tips
✅ Interop Tips
- Use public modules and types in F# (
namespace,module, andtype). - Avoid F#-specific constructs (like inline functions or computation expressions) if they make interop awkward.
- Be cautious with F#’s Option/Result types—they appear differently in C# and may require helpers.
✅ Best Practices
- Keep your domain model IO-free (pure functions, no HttpClient, etc.).
- Write unit tests in F# and integration tests in C#.
- Use shared DTOs or interfaces for clean hand-off between layers if needed.
🚀 Deployment and CI/CD
In the .NET 9 world, you can:
- Build and containerize this hybrid solution using
dotnet publish - Deploy as one unit or split into microservices if scaling dictates
- Write Bicep or Terraform to deploy F# logic wrapped in Azure Functions or C# APIs in App Services
Even AOT scenarios (ahead-of-time compilation) are feasible now for both languages.
📚 Summary
Combining F# and C# in a microservice architecture is a powerful strategy in .NET 9. It lets you:
| Benefit | Description |
|---|---|
| Use the right tool for the job | F# for logic, C# for API/integration |
| Improve testability | Pure logic = easier to test |
| Encourage separation of concerns | F# libraries force a clearer architecture |
| Minimize tech risk | Still one platform, same tooling and deployment |
This approach is clean, maintainable, and makes it easier to onboard developers from either language background. With .NET 9’s performance and deployment enhancements, you can confidently run this hybrid setup at scale.
Views: 23
