Skip to content
The Right Tool for the Job: An Architect's Guide to Leveraging F# and VB.NET in a C#-Dominant World

The Right Tool for the Job: An Architect's Guide to Leveraging F# and VB.NET in a C#-Dominant World

1 Introduction: The Illusion of a Monolingual .NET Ecosystem

The .NET ecosystem is vast, mature, and battle-tested. If you’ve been working within it for any length of time, chances are that your professional reality has revolved around one language: C#. From enterprise web applications to cloud services, desktop tools, and even games, C# has been the lingua franca of .NET for decades. Its ubiquity is so complete that many architects, engineers, and decision-makers implicitly assume that .NET and C# are synonymous.

But here’s the truth: this assumption is an illusion. The .NET ecosystem has always been polyglot at its core. From the earliest days of the Common Language Runtime (CLR), Microsoft designed the platform to be language-agnostic. That design wasn’t just about supporting multiple syntaxes for developer preference—it was about enabling specialized languages to tackle specialized problems more effectively.

Yet, despite this foundation, we’ve built a culture around single-language dominance. The question is—has this mindset been holding us back?

1.1 The C# Supremacy: Acknowledging C# as the powerful, general-purpose default

There’s no denying the strength of C#. It’s an exceptional general-purpose language that has evolved gracefully over the years. From its early Java-like syntax in .NET 1.0 to the pattern-matching, record types, and async capabilities of modern .NET 8, C# has consistently expanded its expressive power without losing its accessibility. It’s backed by a huge ecosystem of libraries, an active community, and first-class tooling in Visual Studio, Rider, and VS Code.

For most teams, C# is the default because it is the safe choice. It’s well-documented, well-understood, and universally recognized. You can hire C# developers in almost any region, and you can trust that the language will continue evolving with Microsoft’s long-term vision for .NET.

The result? The vast majority of enterprise solutions—from small internal tools to massive SaaS platforms—are entirely written in C#. And in most cases, that works. But when the solution space includes problems that C# handles adequately but not optimally, the architect’s role is to ask a bigger question.

1.2 The Architect’s Dilemma: Is the “best practice” always the best solution?

Best practices often start as patterns distilled from experience. They become defaults because they’ve proven to work well in many contexts. But over time, best practices can harden into dogma. When this happens, they can limit creativity and prevent the adoption of better-suited approaches.

Imagine you’re designing a system that involves both high-performance mathematical modeling and a legacy Office automation integration. You can, of course, implement both in C#. You might even write very good C# to handle them. But “good” here doesn’t mean “optimal.” An F# implementation of the mathematical model could reduce complexity and improve correctness. A VB.NET implementation of the Office automation code could reduce boilerplate and increase readability for business stakeholders.

An architect’s job is not to pick the language they personally like best. It’s to select the best tool for each part of the problem—balancing technical fit, maintainability, and the team’s capabilities.

1.3 Introducing Polyglot Programming on .NET

Polyglot programming isn’t about adding complexity for its own sake. It’s about embracing the idea that one language’s strengths can complement another’s weaknesses. In a .NET environment, this approach has almost no technical overhead because of the CLR’s design.

Here’s the thesis: by strategically introducing F# and VB.NET into a C#-dominant architecture, you can solve certain classes of problems more cleanly, safely, and efficiently. This isn’t about abandoning C#. It’s about augmenting it with targeted, specialized power.

This approach works best when you clearly define the boundaries of responsibility. Let C# handle your web APIs, orchestration, and general business logic. Let F# handle complex, immutable domain models and transformation pipelines. Let VB.NET handle business rules that need to be human-readable or code that interfaces with COM automation.

1.4 Meet the Contenders

Before we get into the technical foundation that makes this polyglot approach viable, let’s quickly meet the two languages that most C# architects overlook.

F# — the functional-first powerhouse F# is a multi-paradigm language with a functional-first mindset. Its syntax is concise, its type system is expressive, and its default immutability encourages safe, concurrent programming. It’s particularly good at modeling complex domains, processing data pipelines, and writing algorithms that must be both correct and performant.

VB.NET — the stalwart of stability and readability Visual Basic .NET gets a lot of unfair flak for its association with VB6 and legacy codebases. But modern VB.NET is a fully supported .NET language that evolves alongside C#. Its verbose, English-like syntax makes it extremely readable, and it retains unmatched strengths in areas like COM interop, Office automation, and scenarios where non-developer stakeholders need to read or review the code.

1.5 What This Article Delivers

This guide is written for software architects who are already fluent in C# but are ready to expand their architectural toolkit. We’ll explore:

  • The technical foundation that allows .NET languages to interoperate without friction.
  • The unique strengths of F# and VB.NET in specific architectural contexts.
  • Practical patterns for incorporating these languages into modern, C#-centric solutions.
  • A framework for deciding when to use each language.

By the end, you’ll see that the “.NET means C#” mindset is not just outdated—it may be limiting the potential of your systems.


2 The Foundation: Why Polyglot .NET Just Works

Before we can confidently design a polyglot .NET architecture, we need to understand why it’s possible in the first place. The magic lies in the Common Language Runtime (CLR) and the standards it enforces. This is what ensures that code written in C#, F#, and VB.NET can seamlessly call into each other without glue code, wrappers, or performance compromises.

2.1 The Magic of the CLR (Common Language Runtime)

The CLR is the execution engine for .NET applications. It’s not tied to any single programming language; instead, it’s designed to host multiple languages that follow a set of shared rules and compile into a common intermediate format.

2.1.1 Intermediate Language (IL)

When you compile a .NET program—whether in C#, F#, or VB.NET—the compiler doesn’t produce machine code directly. It produces Intermediate Language (IL), a CPU-agnostic bytecode that the CLR understands. The Just-In-Time (JIT) compiler then translates this IL into native machine code optimized for the specific environment where the application is running.

This shared IL format means that an assembly built from F# source code is no different, from the CLR’s perspective, than one built from C# or VB.NET. As long as it’s CLS-compliant, any .NET language can consume it without extra layers.

2.1.2 Common Type System (CTS)

The CTS defines how types are declared, used, and managed in the runtime. It ensures that an int in C# is exactly the same as an int in F#, which is the same as an Integer in VB.NET. This type unification is what enables direct data exchange without translation.

The CTS also defines rules for inheritance, method overloading, parameter passing, and type visibility. This consistency is why you can pass an F# record to a C# method and have it behave exactly as expected.

2.1.3 Common Language Specification (CLS)

The CLS is a set of rules that all .NET languages agree to follow when they want to be interoperable. These rules are a subset of the full CTS and exist to ensure that public APIs are accessible from any other CLS-compliant language.

For example, the CLS discourages using language-specific features that might not exist in others. If you expose an API in F# that uses a feature not supported by VB.NET, you could run into interoperability issues. By sticking to CLS rules for public interfaces, you guarantee that all languages can consume your code.

2.2 The Practical Result: True, seamless interoperability

Here’s the takeaway: polyglot programming in .NET is not a hack or a risky workaround. It’s a core capability baked into the platform. You can:

  • Write a core domain model in F#.
  • Compile it into a DLL.
  • Reference that DLL from a C# project.
  • Call its functions directly, passing standard .NET types back and forth.

The same goes for VB.NET. If you have a specialized COM interop layer written in VB.NET, your C# application can call into it as if it were written in C#. The CLR takes care of the rest.

There are no translation layers to maintain. There’s no runtime penalty for crossing language boundaries. In many ways, it’s easier to integrate F# and VB.NET into a C# project than it is to integrate JavaScript and TypeScript in a Node.js monorepo—because in .NET, all the languages compile to the exact same intermediate form.


3 Deep Dive: F# — For Complexity, Concurrency, and Correctness

For many C#-dominant teams, F# feels like an exotic neighbor they wave at but never visit. It has a different syntax, different idioms, and—perhaps most intimidatingly—a functional-first mindset. But for the architect willing to explore, F# offers a set of capabilities that can dramatically improve correctness, reduce complexity, and make concurrency safer without the usual pitfalls.

Understanding F# is less about memorizing syntax and more about appreciating a different way of thinking about problems. Once you grasp its mental model, you’ll see why certain workloads are simply more elegant, safer, and faster to implement in F# than in a sprawling C# class hierarchy.

3.1 Thinking in F#: A Paradigm Shift for Architects

F#’s design nudges developers toward writing code that is stateless, declarative, and composable. It’s still a multi-paradigm language—you can write object-oriented code when necessary—but its defaults are functional. This isn’t a quirk of style. It has real architectural implications.

3.1.1 Functional-First, Not Functional-Only

F# embraces functional programming without imposing it dogmatically. You can use classes and mutable state if you really need to, but most F# code avoids them. This is a key distinction from purely functional languages like Haskell, where the functional purity is enforced. In F#, functional patterns are the default, but not the prison.

For architects, this flexibility means you can introduce F# incrementally. Start with functional modeling of core logic where immutability and type safety bring the most benefit, while still allowing OOP-style constructs in integration layers if needed.

3.1.2 Core Principles & Their Architectural Impact

Immutability by Default In F#, values are immutable unless explicitly marked mutable. This drastically reduces the risk of accidental side effects—one of the biggest sources of bugs in concurrent or parallel code. Immutable data structures also simplify reasoning about program state. If a function receives an immutable record, you can be confident that it won’t be altered elsewhere in the system.

Expressions over Statements In F#, almost everything is an expression that returns a value. This makes code inherently more composable. For example, instead of executing a series of commands that modify state, you define transformations from one value to another. It’s the difference between writing a recipe and describing the final dish—clearer, less error-prone, and easier to test.

Powerful Type Inference F#’s type inference is sophisticated enough that you often don’t need to annotate types at all. The compiler infers them from usage, while still guaranteeing static type safety. This results in concise code that retains the safety net of the type system without the verbosity that C# developers sometimes find tedious.

First-Class Functions Functions in F# are values. You can pass them around, return them from other functions, compose them, or partially apply them. This enables higher-order abstractions that are harder to express cleanly in C#. For example, you can build a pipeline of functions that transform data step by step, without introducing intermediate mutable state.

3.2 The Architectural “Sweet Spots” for F#

Not every problem demands F#. But certain domains align perfectly with its strengths. As an architect, these are the places where F# can be a strategic asset.

3.2.1 Complex Data & Domain Modeling

F#’s discriminated unions (DUs) and records allow you to model business domains so that illegal states are unrepresentable. This means the compiler itself helps enforce business rules.

For example, suppose you’re modeling orders in an e-commerce platform. In C#, you might have an Order class with a Status enum and various nullable fields. In F#, you could use a DU to represent the different states of an order, each with only the fields relevant to that state.

type PendingOrder = { Id: int; Items: string list }
type ShippedOrder = { Id: int; TrackingNumber: string }
type DeliveredOrder = { Id: int; DeliveredOn: System.DateTime }

type Order =
    | Pending of PendingOrder
    | Shipped of ShippedOrder
    | Delivered of DeliveredOrder

With this model, you can’t accidentally create a shipped order without a tracking number or a delivered order without a delivery date. The compiler enforces the domain invariants.

3.2.2 Data-Intensive Processing & Transformation Pipelines

ETL jobs, log processing, data cleaning, and scientific computing often involve applying a series of transformations to large datasets. In C#, this often leads to verbose loops and mutable collections. In F#, you can express the same transformations as a pipeline of pure functions.

let cleanData = 
    rawData
    |> List.filter isValid
    |> List.map normalize
    |> List.sortBy key

This is more than just syntactic sugar. The immutability and functional composition reduce the surface area for bugs, and the pipeline style mirrors the actual flow of the transformation, making the code easier to read and maintain.

3.2.3 Mathematical & Financial Modeling

In quantitative finance, risk analysis, and scientific computing, correctness is paramount. A subtle bug can cost millions. F#’s strong typing, immutable data, and concise syntax make it ideal for implementing complex formulas and algorithms in a way that’s both safe and readable.

You can define domain-specific types to prevent unit mismatches, enforce constraints at compile time, and ensure that your equations are implemented exactly as intended.

3.2.4 Concurrent & Asynchronous Systems

Concurrency in C# often involves careful synchronization and defensive programming to avoid race conditions. In F#, immutable data means you don’t need locks for most operations. The MailboxProcessor (an actor model implementation) provides a straightforward way to build message-driven, scalable systems.

let agent = MailboxProcessor.Start(fun inbox ->
    let rec loop state = async {
        let! msg = inbox.Receive()
        let newState = updateState state msg
        return! loop newState
    }
    loop initialState
)

This model naturally isolates state within the agent, avoiding shared mutable state entirely.

3.3 Practical Implementation: An F# Core Logic Library

To see F# in action, let’s build a pricing engine for an e-commerce system—a classic case of complex, evolving business rules where correctness and testability are critical.

3.3.1 Scenario

The pricing engine must handle:

  • Base product prices
  • Tiered volume discounts
  • Seasonal promotions
  • Customer loyalty discounts

These rules should compose cleanly and be easy to modify as the business evolves.

3.3.2 Modeling the Domain

Using discriminated unions and records, we can model the domain so that only valid states are representable.

type CustomerType = Regular | Premium | Wholesale

type ProductCategory = Electronics | Clothing | Grocery

type Discount =
    | Volume of int * decimal // min quantity, discount rate
    | Seasonal of decimal
    | Loyalty of decimal

type Product = { Id: int; Category: ProductCategory; BasePrice: decimal }

type OrderItem = { Product: Product; Quantity: int }

type Order = { CustomerType: CustomerType; Items: OrderItem list }

3.3.3 Creating the Calculation Pipeline

We define small, pure functions for each rule and compose them.

let applyVolumeDiscount (item: OrderItem) =
    match item.Quantity with
    | q when q >= 10 -> item.Product.BasePrice * 0.90M
    | _ -> item.Product.BasePrice

let applySeasonalDiscount price =
    price * 0.95M // 5% seasonal discount

let applyLoyaltyDiscount customerType price =
    match customerType with
    | Premium -> price * 0.90M
    | _ -> price

let calculateItemPrice customerType item =
    item
    |> applyVolumeDiscount
    |> applySeasonalDiscount
    |> applyLoyaltyDiscount customerType

let calculateOrderTotal order =
    order.Items
    |> List.sumBy (calculateItemPrice order.CustomerType)

Because each function is pure, they are easy to test in isolation. Adding a new discount rule is as simple as defining a new function and composing it into the pipeline.

3.3.4 Architectural Benefit

By isolating pricing logic in an F# library, we:

  • Eliminate mutable state and race conditions in concurrent scenarios
  • Create a model that prevents invalid states
  • Enable easy extension by composition
  • Achieve high testability without mocks or complex fixtures

This pricing engine can be consumed by a C# ASP.NET Core API without any special adaptation.

3.4 Addressing the Concerns: The “Buts” of F#

F#’s strengths are compelling, but adoption often stalls due to perceived barriers.

3.4.1 The Learning Curve

Yes, F# looks different. But most developers can become productive in weeks, not months, if they start with focused use cases. Introduce F# in self-contained modules where its benefits are most obvious, like data transformations or rules engines. Pair programming and internal workshops help demystify functional patterns.

3.4.2 Ecosystem & Tooling

As of 2025, F# enjoys excellent tooling in Visual Studio, JetBrains Rider, and VS Code. Language Server Protocol support provides IntelliSense, refactoring, and debugging parity with C#. The F# community has also matured, with rich libraries for data processing, web APIs, and testing.

3.4.3 Hiring & Talent Pool

The F# talent pool is smaller than C#’s, but that’s not necessarily a blocker. Internal upskilling can be highly effective, especially since most .NET developers already understand the runtime and tooling. Target F# for the parts of the system where its benefits are clearest, so you don’t need every team member to be an expert.


4 Deep Dive: VB.NET — For Stability, Readability, and Integration

If F# is the precision instrument in a .NET architect’s toolkit, VB.NET is the dependable workhorse that’s often underestimated. In a C#-dominated ecosystem, VB.NET rarely makes it into architectural discussions—more often than not because of outdated perceptions rather than technical shortcomings. Yet, for certain problem domains, it remains one of the most practical choices available.

The key to appreciating VB.NET is to set aside the baggage of VB6 nostalgia and instead look at what the language actually offers today. It is modern, supported, and purpose-built for scenarios where clarity, maintainability, and integration with legacy systems are non-negotiable.

4.1 Re-evaluating VB.NET in the Modern Era

4.1.1 Beyond the Legacy Label

It’s easy to assume that VB.NET is somehow obsolete because of its association with VB6, but this is both inaccurate and unfair. VB.NET is not VB6. It has been a first-class .NET language since the beginning, sharing the same runtime, type system, and performance characteristics as C# and F#. Microsoft has continued to invest in it, releasing updates in lockstep with .NET platform changes.

The truth is that VB.NET is still viable for modern application development, particularly where its strengths align with architectural goals. It can consume and produce the same assemblies as C# or F#. It can be part of the same solution file, compiled to the same IL, and run on the same runtime. The difference lies in its syntax, readability, and legacy integration capabilities.

4.1.2 Core Principles & Their Architectural Impact

Unmatched Readability VB.NET’s syntax is verbose, with explicit keywords and minimal symbolic noise. In contexts where non-developer stakeholders—such as business analysts, compliance officers, or domain experts—need to review the code, this is a significant advantage. For example, a VB.NET If…Then…Else block is easier for a non-technical reader to follow than C#’s { }-delimited style.

Stability & Backward Compatibility Microsoft has consistently stated that VB.NET’s long-term value lies in stability. While C# pushes forward with new syntactic features at a rapid pace, VB.NET evolves more conservatively. For long-lived systems where the priority is not chasing the latest features but ensuring consistent behavior, this stability is reassuring.

Excellent COM Interop & Office Automation VB.NET retains its heritage as an excellent choice for automating Office applications, working with COM components, and interacting with legacy Windows APIs. This isn’t just about convenience—it’s about reducing the boilerplate and complexity that C# often requires for the same tasks.

4.2 The Architectural “Sweet Spots” for VB.NET

4.2.1 Business Rules Engines (BREs)

When the business logic must be not only correct but also understandable to non-developers, VB.NET shines. Its verbose syntax maps closely to plain English, making it easier for business analysts to validate that the implemented rules match the documented ones. This is especially important in regulated industries such as insurance, finance, and healthcare.

A rules engine in VB.NET can often serve as both executable logic and living documentation.

4.2.2 Rapid Application Development (RAD) for Internal Tools

VB.NET’s strong WinForms and WPF support makes it ideal for quickly building internal line-of-business applications. In scenarios where speed of delivery is more important than leveraging the latest UI frameworks, VB.NET allows teams to deliver functional, maintainable desktop tools without unnecessary complexity.

4.2.3 Strategic Modernization

For organizations with a significant investment in VB6 applications, VB.NET offers the smoothest migration path to .NET. Syntax similarities reduce the retraining burden for developers, and code can often be ported incrementally. This approach minimizes risk while gradually bringing legacy systems into a modern, maintainable architecture.

4.2.4 Heavy Office/COM Integration

Automating Excel reports, generating Word documents, or controlling Outlook workflows is significantly less verbose in VB.NET than in C#. The language handles COM interop more gracefully, reducing ceremony and making scripts easier to write and maintain.

4.3 Practical Implementation: A VB.NET Business Rules Module

4.3.1 Scenario

Consider an insurance underwriting system where eligibility rules change frequently. The rules are defined by underwriters, not developers, and need to be validated by business stakeholders before deployment. In this case, the clarity of VB.NET’s syntax allows the rules module to serve as both executable logic and an easily reviewable artifact.

4.3.2 Modeling the Rules

Here’s an example VB.NET class implementing such rules:

Public Class PolicyEligibility

    Public Function IsEligible(applicant As Applicant) As Boolean
        If applicant.Age < 18 Then
            Return False
        ElseIf applicant.Age > 65 Then
            Return False
        End If

        Select Case applicant.PolicyType
            Case PolicyType.Health
                If applicant.PreExistingConditions.Count > 2 Then
                    Return False
                End If
            Case PolicyType.Life
                If applicant.Smoker = True Then
                    Return False
                End If
            Case PolicyType.Auto
                If applicant.DrivingRecord.Points > 6 Then
                    Return False
                End If
        End Select

        Return True
    End Function

End Class

This code is clear, explicit, and almost self-documenting. It lacks syntactic noise such as braces or semicolons, and the use of full words (End If, Select Case) makes it approachable for non-technical reviewers.

4.3.3 Architectural Benefit

By isolating the rules in a VB.NET module:

  • The business logic can be reviewed by non-developers, reducing misinterpretation risk.
  • The rules can be changed independently of the rest of the system, allowing faster iteration.
  • The explicit syntax lowers the barrier for stakeholders to engage in code reviews, increasing confidence in the implementation.

This module could be compiled into a .NET assembly and consumed directly by a C# API or service without any interoperability overhead.

4.4 Addressing the Concerns: The “Buts” of VB.NET

4.4.1 The “Coolness” Factor & Community Perception

Let’s be honest—VB.NET is not considered “cool” by most modern developers. This perception stems from its association with VB6 and beginner programming. However, as architects, decisions should be driven by technical merit, not fashion. In contexts where VB.NET’s strengths are relevant, its lack of hype is irrelevant.

4.4.2 Lagging Feature Adoption

VB.NET does not always receive new language features as quickly as C#. For instance, while C# might adopt new pattern matching capabilities or minimal APIs sooner, VB.NET focuses on maintaining consistency and backward compatibility. For most architectural use cases—business rules, COM interop, legacy migration—these missing syntactic sugars are not critical.

4.4.3 Future Trajectory

Microsoft’s stated strategy for VB.NET is “co-evolution” with C#, meaning that while the two languages will share platform-level capabilities (like .NET runtime features), their syntax and evolution paths may differ. For long-term projects, this means VB.NET will remain a safe choice for its niches, but teams should not expect it to become a cutting-edge experimental language.


5 The Polyglot in Practice: Hybrid Architectural Patterns

The theory behind polyglot .NET is compelling, but architecture is judged on execution. In practice, adopting multiple languages within the same solution requires discipline and a clear separation of responsibilities. Without this structure, you risk blurring the boundaries between languages, increasing cognitive load, and confusing your development team.

Two patterns consistently emerge as the most effective ways to leverage F# and VB.NET within a C#-dominant architecture:

  • The F# Core, C# Shell pattern — functional precision at the heart of a scalable C# application.
  • The VB.NET Specialist Library pattern — encapsulating integration complexity behind a clean, consumable interface.

These patterns are repeatable, maintainable, and align naturally with how the CLR enables cross-language interoperability.

5.1 The “F# Core, C# Shell” Pattern

5.1.1 Description

The F# Core, C# Shell pattern is straightforward:

  • C# handles the presentation layer, web endpoints, infrastructure plumbing, dependency injection, and orchestration.
  • F# handles the core domain logic, computation-heavy tasks, and data transformations.

Think of it as a two-layer cake: the C# layer provides all the ingredients and delivers the final dish, but the actual cooking—where precision matters—is done in F#. By structuring the solution this way, you keep the benefits of C#’s vast ecosystem and developer familiarity, while harnessing F#’s strengths for correctness and conciseness where it counts.

5.1.2 Diagram

[Conceptual Flow]

  1. HTTP Request → Received by ASP.NET Core Controller (C#).
  2. Controller validates input and maps DTO to domain model.
  3. Controller calls F# library (via a direct method call on a referenced assembly).
  4. F# core logic executes, returns a result or error object.
  5. Controller maps result to HTTP response and sends it back.
[HTTP Request]  

[C# Controller Layer] — Validation, DTO mapping, orchestration  

[F# Core Library] — Pure domain logic, calculations, transformations  

[C# Controller Layer] — Result mapping, HTTP response  

[HTTP Response]

This boundary is clean: all domain rules live in F#. C# never mutates the state of the domain directly.

5.1.3 Implementation Steps

Step 1: Set up the solution in Visual Studio
  • Create a new ASP.NET Core Web API project in C# (e.g., MyApp.Api).
  • Create a new Class Library project in F# (e.g., MyApp.Core).
  • In MyApp.Api, add a reference to MyApp.Core.

Your solution structure will look like this:

MyApp.Api  (C#)
MyApp.Core (F#)
Step 2: Create F# domain logic

In MyApp.Core, define a pricing engine function (continuing from earlier examples):

namespace MyApp.Core

type CustomerType = Regular | Premium | Wholesale

type Product = { Id: int; BasePrice: decimal }
type OrderItem = { Product: Product; Quantity: int }
type Order = { CustomerType: CustomerType; Items: OrderItem list }

module Pricing =
    let private applyVolumeDiscount item =
        match item.Quantity with
        | q when q >= 10 -> item.Product.BasePrice * 0.90M
        | _ -> item.Product.BasePrice

    let private applyLoyaltyDiscount customerType price =
        match customerType with
        | Premium -> price * 0.85M
        | Wholesale -> price * 0.80M
        | _ -> price

    let calculateTotal order =
        order.Items
        |> List.sumBy (fun i -> i |> applyVolumeDiscount |> applyLoyaltyDiscount order.CustomerType)

This module contains pure functions. There are no side effects, no mutable state, and no dependency on C# types except standard .NET primitives.

Step 3: Call F# from C#

In the MyApp.Api project, define a DTO and controller that calls the F# library:

namespace MyApp.Api.Controllers
{
    using Microsoft.AspNetCore.Mvc;
    using MyApp.Core;

    public record ProductDto(int Id, decimal BasePrice);
    public record OrderItemDto(ProductDto Product, int Quantity);
    public record OrderDto(string CustomerType, List<OrderItemDto> Items);

    [ApiController]
    [Route("api/[controller]")]
    public class PricingController : ControllerBase
    {
        [HttpPost("calculate")]
        public IActionResult Calculate([FromBody] OrderDto dto)
        {
            var customerType = dto.CustomerType switch
            {
                "Premium" => CustomerType.Premium,
                "Wholesale" => CustomerType.Wholesale,
                _ => CustomerType.Regular
            };

            var items = dto.Items.Select(i =>
                new OrderItem(
                    new Product(i.Product.Id, i.Product.BasePrice),
                    i.Quantity
                )
            ).ToList();

            var order = new Order(customerType, items);
            var total = Pricing.calculateTotal(order);

            return Ok(new { Total = total });
        }
    }
}

Notice how the C# code simply maps incoming HTTP data into F# domain types, calls the F# function, and returns the result. There’s no duplication of pricing logic—F# owns it entirely.

5.2 The “VB.NET Specialist Library” Pattern

5.2.1 Description

The VB.NET Specialist Library pattern isolates messy or verbose integration code—often involving COM interop or legacy APIs—inside a dedicated VB.NET assembly. This assembly exposes a clean, simple interface that can be consumed from the rest of the application, which is typically in C#.

The goal is to keep complexity localized. VB.NET’s syntax and language features make it particularly good at handling Office automation and older Windows APIs with minimal boilerplate.

5.2.2 Diagram

[Conceptual Flow]

  1. C# Application (microservice, desktop app, or web job) needs to generate an Excel report.
  2. Instead of embedding interop code directly in C#, it calls a single method on a VB.NET library.
  3. The VB.NET library handles all COM object creation, formatting, and cleanup.
  4. VB.NET returns a simple result (file path, byte array) to the C# caller.
[C# Service] — High-level orchestration  

[VB.NET Library] — COM automation, Excel formatting, interop handling  

[Excel COM API] — Legacy integration  

This pattern ensures that the inevitable quirks of COM programming are confined to a single, well-documented assembly.

5.2.3 Implementation Steps

Step 1: Create the VB.NET project

Add a new VB.NET Class Library to your solution, e.g., MyApp.Reporting.

Step 2: Implement Excel report generation in VB.NET
Imports Microsoft.Office.Interop

Public Class ExcelReportGenerator

    Public Function GenerateSalesReport(data As List(Of SalesRecord), outputPath As String) As String
        Dim excelApp As New Excel.Application
        Dim workbook As Excel.Workbook = excelApp.Workbooks.Add()
        Dim sheet As Excel.Worksheet = workbook.Sheets(1)

        ' Headers
        sheet.Cells(1, 1).Value = "Product"
        sheet.Cells(1, 2).Value = "Quantity"
        sheet.Cells(1, 3).Value = "Total"

        ' Data
        Dim row As Integer = 2
        For Each record In data
            sheet.Cells(row, 1).Value = record.ProductName
            sheet.Cells(row, 2).Value = record.Quantity
            sheet.Cells(row, 3).Value = record.Total
            row += 1
        Next

        workbook.SaveAs(outputPath)
        workbook.Close()
        excelApp.Quit()

        Return outputPath
    End Function

End Class

This VB.NET code is explicit and avoids the verbosity that similar COM code in C# would require. The For Each loop reads naturally, and Dim declarations are easy to follow.

Step 3: Call VB.NET from C#

In your C# application:

var generator = new MyApp.Reporting.ExcelReportGenerator();
var outputPath = Path.Combine("C:\\Reports", "sales.xlsx");

var data = new List<SalesRecord>
{
    new SalesRecord("Widget A", 10, 200.00m),
    new SalesRecord("Widget B", 5, 150.00m)
};

var reportPath = generator.GenerateSalesReport(data, outputPath);
Console.WriteLine($"Report generated at: {reportPath}");

The C# code remains clean, delegating all interop complexity to the VB.NET library.

Architectural Benefits of These Patterns

Both patterns—F# Core, C# Shell and VB.NET Specialist Library—share common advantages:

  • Isolation of concerns: Each language is used for the scenarios where it excels.
  • Maintainability: Changes to the F# or VB.NET libraries don’t ripple through the rest of the application.
  • Interoperability with zero runtime cost: The CLR ensures seamless integration.
  • Team flexibility: Not every developer needs to master all three languages; work can be divided according to skill and interest.

6 An Architect’s Decision Framework

When building modern .NET systems, the biggest mistake an architect can make is defaulting to a single language for every problem simply because it’s “standard practice.” The right tool for the job isn’t about loyalty to C#, F#, or VB.NET. It’s about evaluating the nature of the problem, the capabilities of your team, and the non-functional requirements of your system, then choosing accordingly.

The purpose of this framework is to give you a repeatable way to make those decisions. It moves the discussion from abstract “feature lists” to concrete factors that directly affect architecture quality and long-term maintainability.

6.1 Asking the Right Questions: Moving from Features to Factors

Before choosing a language for a specific part of your system, ask these questions in sequence. The answer to each will guide your selection.

1. What is the problem domain? Are you building a standard CRUD API? A complex financial simulation? A rules engine that non-developers must validate? The problem domain heavily influences the right language choice.

2. What is the primary non-functional requirement? Is your highest priority correctness, productivity, readability, or stability? For example, in a mission-critical banking algorithm, correctness outweighs all else—pointing strongly toward F#.

3. What is your team’s skill profile? Do you have functional programming experience in-house? Do you have developers with VB heritage? Is your team purely C#-focused? The ramp-up cost must be weighed against potential gains.

4. What concurrency model will you use? Does your system require high-scale, immutable-by-default parallel processing? Or will it rely on standard async/await and locks? Your concurrency needs can influence whether C# or F# is the better fit.

5. How important is long-term maintainability in this component? Will the component need to be maintained for a decade? Will it be reviewed by non-developers? This can push you toward more concise (F#) or more readable (VB.NET) solutions.

6. What’s the interoperability requirement? Will the component need to interoperate tightly with COM APIs, Office automation, or other legacy systems? If so, VB.NET’s strengths become hard to ignore.

These questions move you away from “Which language do we like?” toward “Which language fits the architecture best?”

6.2 The Decision Matrix: A Table-Based Guide for Architects

The table below summarizes where each language shines across key architectural factors.

FactorC# (General Purpose)F# (Specialized Power)VB.NET (Specialized Stability)
Problem DomainWeb APIs, CRUD apps, general business logicData transformation, complex domains, concurrency, mathBusiness rules, Office automation, legacy interop
Primary GoalProductivity, large ecosystem, familiarityCorrectness, robustness, conciseness, parallelismReadability, stability, rapid internal tools
Team SkillsetWidely availableNiche, requires investment in functional paradigmLower barrier for entry, familiar to classic VB devs
Concurrency Modelasync/await, Task, locking primitivesImmutable by default, Actors (MailboxProcessor), asyncSame as C#, but less idiomatic for complex scenarios
Long-Term Maint.Excellent, but verbosity can hide bugs in complex logicExcellent, highly refactorable, less code is fewer bugsExcellent for its niche, code is easy to understand years later

Export to Sheets: This decision matrix works best when embedded into a collaborative tool like Google Sheets or Excel so that architects can score options for each component and arrive at a data-driven decision.

6.3 Final Heuristics

From the matrix and factors above, we can distill a set of practical heuristics for everyday architectural decision-making:

  • If correctness is your paramount non-functional requirement… start with F#. Its type system, immutability, and functional composition reduce the risk of subtle, costly bugs.
  • If you’re interfacing with the Microsoft Office ecosystem or a COM API… start with VB.NET. It simplifies interop code and reduces boilerplate.
  • If you’re building a standard web application… start with C#, but ask if any component within it fits the F# or VB.NET profile. Mixing languages where appropriate can yield better long-term results.

A Detailed Example: Applying the Framework

Imagine you’re designing a Financial Portfolio Management System. The requirements are:

  • Web-based dashboard for users to manage portfolios (CRUD operations, C# friendly).
  • Complex risk analysis engine with multi-step calculations that must be correct to the decimal (F# sweet spot).
  • Quarterly PDF and Excel report generation that integrates with Office templates used by compliance teams (VB.NET’s strength in COM automation).

Following the framework:

  1. Problem Domain:

    • CRUD → C#
    • Complex calculations → F#
    • Office integration → VB.NET
  2. Primary Goal:

    • For CRUD → productivity and rapid delivery → C#
    • For calculations → correctness and robustness → F#
    • For reporting → readability and integration stability → VB.NET
  3. Team Skillset:

    • C# knowledge is universal among the team.
    • One developer with functional programming background can lead the F# core library.
    • Two developers with past VB6 experience can own VB.NET reporting module.
  4. Concurrency Model:

    • CRUD layer: standard async/await → C#
    • Risk engine: immutable data + actor model → F#
    • Reporting: synchronous COM calls (acceptable in background job) → VB.NET
  5. Long-Term Maintenance:

    • C# web layer will evolve with .NET → maintainable with updates.
    • F# calculation library: minimal churn expected, safe due to compiler checks.
    • VB.NET reporting: stable templates, low change frequency.

How This Plays Out in Code

C# Controller Calling F# Core Logic

[ApiController]
[Route("api/[controller]")]
public class RiskController : ControllerBase
{
    [HttpPost("calculate")]
    public IActionResult CalculateRisk([FromBody] PortfolioDto dto)
    {
        var portfolio = DtoMapper.MapToFSharpPortfolio(dto);
        var riskScore = MyApp.Core.RiskEngine.calculateRisk(portfolio);
        return Ok(new { RiskScore = riskScore });
    }
}

F# Risk Engine

namespace MyApp.Core

type Asset = { Symbol: string; Quantity: decimal; Price: decimal }
type Portfolio = { Assets: Asset list }

module RiskEngine =
    let private calculateAssetRisk asset =
        match asset.Symbol with
        | "GOVBOND" -> asset.Quantity * asset.Price * 0.1M
        | "EQUITY" -> asset.Quantity * asset.Price * 0.3M
        | _ -> asset.Quantity * asset.Price * 0.2M

    let calculateRisk portfolio =
        portfolio.Assets
        |> List.map calculateAssetRisk
        |> List.sum

VB.NET Reporting Module

Imports Microsoft.Office.Interop

Public Class ReportGenerator
    Public Function GenerateQuarterlyReport(portfolio As Portfolio, templatePath As String, outputPath As String) As String
        Dim excelApp As New Excel.Application
        Dim workbook As Excel.Workbook = excelApp.Workbooks.Open(templatePath)
        Dim sheet As Excel.Worksheet = workbook.Sheets(1)

        Dim row As Integer = 2
        For Each asset In portfolio.Assets
            sheet.Cells(row, 1).Value = asset.Symbol
            sheet.Cells(row, 2).Value = asset.Quantity
            sheet.Cells(row, 3).Value = asset.Price
            row += 1
        Next

        workbook.SaveAs(outputPath)
        workbook.Close()
        excelApp.Quit()
        Return outputPath
    End Function
End Class

In this architecture, each language is used where it is strongest. The system remains easy to reason about because the responsibilities are clear and language boundaries align with natural component boundaries.


7 Conclusion: The Mature Architect’s View

Polyglot programming in .NET is not a novelty act. It is not an academic exercise, nor is it a distraction from “real” engineering. It is, at its core, an acknowledgment that the platform we work on every day was designed to support multiple languages for a reason. The Common Language Runtime (CLR), the Common Type System (CTS), and the Common Language Specification (CLS) are not historical footnotes—they are living enablers for better solutions.

A mature architect understands that language choice is not about personal preference or nostalgia. It is a deliberate trade-off between competing priorities. Every language carries strengths and constraints, and every problem domain presents unique challenges. Your role is to align those two realities.

7.1 Beyond Language Wars: Choosing a language is an engineering trade-off, not a statement of identity

The software industry has a long history of framing language choice as a kind of tribal affiliation. You’re a “C# developer,” an “F# enthusiast,” or a “VB die-hard.” While such labels might be convenient shorthand, they can also limit thinking. Architecture is about outcomes, not identity.

When you choose C#, you are prioritizing ecosystem breadth, familiarity, and proven productivity. When you choose F#, you are prioritizing correctness, conciseness, and immutability. When you choose VB.NET, you are prioritizing readability, stability, and integration with legacy systems. None of these choices is “right” or “wrong” in isolation—they only make sense relative to the problem at hand.

A seasoned architect resists the temptation to force every problem into the same mold. Instead, they ask, “Which tool fits the job best?” and act accordingly.

7.2 The .NET Platform, Not the C# Platform: Embracing the full power of the runtime Microsoft has built

It’s easy to forget that .NET was never meant to be synonymous with C#. From day one, Microsoft envisioned a multi-language ecosystem. The CLR doesn’t care what source language your code was written in. IL is IL, whether it came from curly braces, indentation, or English-like keywords.

When you think of .NET as the platform—not just the C# platform—you unlock opportunities that single-language thinking obscures. You can:

  • Let C# handle your high-traffic ASP.NET Core API endpoints.
  • Let F# own your most complex domain models or concurrent processing logic.
  • Let VB.NET simplify your Office automation or legacy COM interop layers.

These aren’t compromises—they’re optimizations. You’re not weakening your architecture by using multiple languages; you’re making it stronger by letting each part of the system be expressed in the language that handles it best.

7.3 Final Call to Action

The next time you start a project—or even a new module in an existing one—pause before creating that default C# Class Library. Ask yourself:

“Is there a specific, specialized workload here that could be solved more elegantly, more safely, or more clearly with F# or VB.NET?”

If the answer is yes, take the step. Set up the extra project. Reference it from your main solution. Embrace the reality that you are building for the .NET ecosystem, not just for a single language.

Small decisions accumulate into architectural style. A willingness to apply F# for correctness-critical logic, or VB.NET for business rules that non-developers must read, sends a message to your team: we choose technology intentionally. We solve problems with precision, not habit.

By doing so, you not only honor the design vision of the .NET platform but also elevate your solutions from merely good to truly great. You stop being just a C# architect and become a .NET architect—someone who uses the full spectrum of tools available to craft systems that are robust, maintainable, and fit for purpose.

Advertisement