In my previous post (or “mental note,” as I like to call them), I covered how to set up multi-tenancy in Oqtane. Today, I got a really nice surprise — Shaun Walker just posted an excellent video explaining how multi-tenancy works,
along with its advantages and possible drawbacks.
From my point of view, the advantages clearly outweigh the disadvantages,
although it depends on your specific scenario.
Extending the Previous Example
I wanted to improve my previous example a bit. So, I created a new GitHub repository using the same base code,
but this time I added hostnames for each tenant.
A hostname is basically the domain that points to one of your tenants in Oqtane.
In a typical setup, you use DNS records for this.
The simplest case is an A record that points to a specific IP address.
When a request arrives, the server reads the hostname from the request and routes it to the correct tenant.
This part isn’t specific to Oqtane — it’s how web servers work in general.
The concept exists in IIS, Apache, and NGINX,
and it’s part of basic networking theory. If you want to learn more,
there are countless articles about how DNS works.
A Small Story from the Past
This actually takes me back — one of the first things I learned as a teenager was how to configure DNS
and run my own Apache web server.
I even started offering web hosting from my home using an old 486 computer (yes, really).
Eventually, my internet provider noticed what I was doing, blocked my connection, and called my parents.
Let’s just say… that Christmas was canceled for me. 😅
Anyway, that’s a story for another time.
Setting Up Local Domains for Tenants
For today’s example, I’m using the same structure as before:
One host site
Two tenant sites: MyCompany1 and MyCompany2
I want to show you how to assign domain names to each of them.
If you’re running everything locally (for example, through Visual Studio or VS Code),
you can’t use real domain names — but you can simulate them using the Windows hosts file.
If you’ve ever wondered how your computer resolves localhost to 127.0.0.1,
the answer lies in that file. It’s located inside the Windows system folder,
and it maps domain names to IP addresses.
Here’s the cool part: you can add your own domains there, pointing them to any IP you like.
It’s a great trick for local testing.
Below, you’ll see a screenshot of my hosts file.
I’ve mapped my fake domains to my local IP address,
so when I open them in the browser, the requests go straight to my Kestrel server, which then routes them to the correct tenant.
How to Edit the Windows Hosts File
Editing the hosts file in Windows is simple, but you need administrative permissions.
Here’s how you can do it safely:
Press Start, type Notepad, then right-click it and select Run as administrator.
Once Notepad opens, go to File → Open and browse to:
C:\Windows\System32\drivers\etc\hosts
In the open dialog, change the filter from “Text Documents (*.txt)” to “All Files (*.*)”
so you can see the hosts file.
Add your entries at the bottom of the file. For example:
127.0.0.1 mycompany1.xyz
127.0.0.1 mycompany2.xyz
Each line maps an IP address to a domain name.
Save the file and close Notepad.
Open your browser and visit http://mycompany1.xyz:44398
(or the port your Oqtane app is running on).
You should see the tenant corresponding to that domain.
⚠️ Important: If you edit the file without admin rights,
you won’t be able to save it. Also, be careful — if you modify or delete system entries by accident,
your network resolution might stop working.
Here is how my host file actually looks at the moment
Set siteURL for :Company 1
Set siteURL for :Company 2
Testing with Real Domains
Of course, this same logic applies to real domains too — as long as your Oqtane instance is publicly accessible.
In one of the next parts (maybe part 3 or 4), I’ll show how to configure it using a web server like Apache. I know that NGINX is more popular these days,
but I’ve used Apache since my teenage years, so I’m more comfortable with it.
Still, I’ll probably demonstrate both.
Most developers today use cloud providers like AWS or Azure,
but honestly, I still prefer spinning up a simple Ubuntu server and doing everything manually.
The best tool is the one you know best — and for me, that’s Apache on Ubuntu.
Demo
As you can see there is a little bit of a different behavior if is a default site or not If it’s a default site it will redirect to that URL if not it’s going to redirect to the default site URL
Resources
🧩 GitHub Repository — This project is based on the previous example
but adds hostname configuration and uses SQLite for simplicity.
If you hang out around developers long enough, you’ll notice we don’t just use tools — we nickname them, mispronounce them, and sometimes turn them into full-blown mascots. Here are three favorites: WSL, SQL, and GitHub Copilot’s Spec Kit.
WSL → “Weasel”
English reality: WSL stands for Windows Subsystem for Linux.
Nickname: Said quickly as “double-u S L,” it echoes weasel, so the meme stuck.
Spanish (El Salvador / Latin America): In El Salvador and many Latin American countries, the letter W is read as “doble be” (not doble u). So WSL is pronounced “doble be, ese, ele.”
SQL → “Sequel”
English reality: SQL stands for Structured Query Language.
Pronunciation: Both “S-Q-L” and “sequel” are used in English.
Spanish (LatAm): Most developers say it letter by letter: “ese cu e ele.” Bilingual teams sometimes mix in “sequel.”
Spec Kit → “Speckified” (Spooky Spell)
English reality: GitHub Copilot’s Spec Kit helps scaffold code from specs.
Community fun: Projects get “speckified,” a word that mischievously echoes “spookified.” Our playful mascot idea is a wizard enchanting a codebase: You have been Speckified!
Spanish (LatAm): Phonetically, SPEC is “ese, pe, e, ce.” In casual talk many devs just say “espec” (es-pek) to keep the pun alive.
Quick Reference (Latin American / El Salvador Spanish)
Acronym
English Pronunciation
Spanish (LatAm / El Salvador) Phonetics
Nickname / Mascot
WSL
“double-u S L” (sounds like weasel)
“doble be, ese, ele”
Weasel
SQL
“S-Q-L” or “sequel”
“ese cu e ele”
Sequel Robot
SPEC
“spec” → “speckified”
“ese, pe, e, ce” (or “espec”)
Spec Wizard (spell)
Why This Matters
These playful twists — weasel, sequel robot, speckified wizard — show how dev culture works:
Acronyms turn into characters.
English vs. Spanish pronunciations add layers of humor.
Memes make otherwise dry tools easier to talk about.
Next time someone says their project is fully speckified on WSL with SQL, you might be hearing about a weasel, a robot, and a wizard casting spooky spec spells.
Integration testing is a critical phase in software development where individual modules are combined and tested as a group. In our accounting system, we’ve created a robust integration test that demonstrates how the Document module and Chart of Accounts module interact to form a functional accounting system. In this post, I’ll explain the components and workflow of our integration test.
The Architecture of Our Integration Test
Our integration test simulates a small retail business’s accounting operations. Let’s break down the key components:
Test Fixture Setup
The AccountingIntegrationTests class contains all our test methods and is decorated with the [TestFixture] attribute to identify it as a NUnit test fixture. The Setup method initializes our services and data structures:
[SetUp]
public async Task Setup()
{
// Initialize services
_auditService = new AuditService();
_documentService = new DocumentService(_auditService);
_transactionService = new TransactionService();
_accountValidator = new AccountValidator();
_accountBalanceCalculator = new AccountBalanceCalculator();
// Initialize storage
_accounts = new Dictionary<string, AccountDto>();
_documents = new Dictionary<string, IDocument>();
_transactions = new Dictionary<string, ITransaction>();
// Create Chart of Accounts
await SetupChartOfAccounts();
}
This method:
Creates instances of our services
Sets up in-memory storage for our entities
Calls SetupChartOfAccounts() to create our initial chart of accounts
Chart of Accounts Setup
The SetupChartOfAccounts method creates a basic chart of accounts for our retail business:
private async Task SetupChartOfAccounts()
{
// Clear accounts dictionary in case this method is called multiple times
_accounts.Clear();
// Assets (1xxxx)
await CreateAccount("Cash", "10100", AccountType.Asset, "Cash on hand and in banks");
await CreateAccount("Accounts Receivable", "11000", AccountType.Asset, "Amounts owed by customers");
// ... more accounts
// Verify all accounts are valid
foreach (var account in _accounts.Values)
{
bool isValid = _accountValidator.ValidateAccount(account);
Assert.That(isValid, Is.True, $"Account {account.AccountName} validation failed");
}
// Verify expected number of accounts
Assert.That(_accounts.Count, Is.EqualTo(17), "Expected 17 accounts in chart of accounts");
}
This method:
Creates accounts for each category (Assets, Liabilities, Equity, Revenue, and Expenses)
Validates each account using our AccountValidator
Ensures we have the expected number of accounts
Individual Transaction Tests
We have separate test methods for specific transaction types:
Purchase of Inventory
CanRecordPurchaseOfInventory demonstrates recording a supplier invoice:
[Test]
public async Task CanRecordPurchaseOfInventory()
{
// Arrange - Create document
var document = new DocumentDto { /* properties */ };
// Act - Create document, transaction, and entries
var createdDocument = await _documentService.CreateDocumentAsync(document, TEST_USER);
// ... create transaction and entries
// Validate transaction
var isValid = await _transactionService.ValidateTransactionAsync(
createdTransaction.Id, ledgerEntries);
// Assert
Assert.That(isValid, Is.True, "Transaction should be balanced");
}
Validates that the transaction is balanced (debits = credits)
Sale to Customer
CanRecordSaleToCustomer demonstrates recording a customer sale:
[Test]
public async Task CanRecordSaleToCustomer()
{
// Similar pattern to inventory purchase, but with sale-specific entries
// ...
// Create ledger entries - a more complex transaction with multiple entries
var ledgerEntries = new List<ILedgerEntry>
{
// Cash received
// Sales revenue
// Cost of goods sold
// Reduce inventory
};
// Validate transaction
// ...
}
This test is more complex, recording both the revenue side (debit Cash, credit Sales Revenue) and the cost side (debit Cost of Goods Sold, credit Inventory) of a sale.
Full Accounting Cycle Test
The CanExecuteFullAccountingCycle method ties everything together:
[Test]
public async Task CanExecuteFullAccountingCycle()
{
// Run these in a defined order, with clean account setup first
_accounts.Clear();
_documents.Clear();
_transactions.Clear();
await SetupChartOfAccounts();
// 1. Record inventory purchase
await RecordPurchaseOfInventory();
// 2. Record sale to customer
await RecordSaleToCustomer();
// 3. Record utility expense
await RecordBusinessExpense();
// 4. Create a payment to supplier
await RecordPaymentToSupplier();
// 5. Verify account balances
await VerifyAccountBalances();
}
This test:
Starts with a clean state
Records a sequence of business operations
Verifies the final account balances
Mock Account Balance Calculator
The MockAccountBalanceCalculator is a crucial part of our test that simulates how a real database would work:
public class MockAccountBalanceCalculator : AccountBalanceCalculator
{
private readonly Dictionary<string, AccountDto> _accounts;
private readonly Dictionary<Guid, List<LedgerEntryDto>> _ledgerEntriesByTransaction = new();
private readonly Dictionary<Guid, decimal> _accountBalances = new();
public MockAccountBalanceCalculator(
Dictionary<string, AccountDto> accounts,
Dictionary<string, ITransaction> transactions)
{
_accounts = accounts;
// Create mock ledger entries for each transaction
InitializeLedgerEntries(transactions);
// Calculate account balances based on ledger entries
CalculateAllBalances();
}
// Methods to initialize and calculate
// ...
}
This class:
Takes our accounts and transactions as inputs
Creates a collection of ledger entries for each transaction
Calculates account balances based on these entries
Provides methods to query account balances and ledger entries
The InitializeLedgerEntries method creates a collection of ledger entries for each transaction:
private void InitializeLedgerEntries(Dictionary<string, ITransaction> transactions)
{
// For inventory purchase
if (transactions.TryGetValue("InventoryPurchase", out var inventoryPurchase))
{
var entries = new List<LedgerEntryDto>
{
// Create entries for this transaction
// ...
};
_ledgerEntriesByTransaction[inventoryPurchase.Id] = entries;
}
// For other transactions
// ...
}
The CalculateAllBalances method processes these entries to calculate account balances:
private void CalculateAllBalances()
{
// Initialize all account balances to zero
foreach (var account in _accounts.Values)
{
_accountBalances[account.Id] = 0m;
}
// Process each transaction's ledger entries
foreach (var entries in _ledgerEntriesByTransaction.Values)
{
foreach (var entry in entries)
{
if (entry.EntryType == EntryType.Debit)
{
_accountBalances[entry.AccountId] += entry.Amount;
}
else // Credit
{
_accountBalances[entry.AccountId] -= entry.Amount;
}
}
}
}
This approach closely mirrors how a real accounting system would work with a database:
Ledger entries are stored in collections (similar to database tables)
Account balances are calculated by processing all relevant entries
The calculator provides methods to query this data (similar to a repository)
Balance Verification
The VerifyAccountBalances method uses our mock calculator to verify account balances:
private async Task VerifyAccountBalances()
{
// Create mock balance calculator
var mockBalanceCalculator = new MockAccountBalanceCalculator(_accounts, _transactions);
// Verify individual account balances
decimal cashBalance = mockBalanceCalculator.CalculateAccountBalance(
_accounts["Cash"].Id,
_testDate.AddDays(15)
);
Assert.That(cashBalance, Is.EqualTo(-2750m), "Cash balance is incorrect");
// ... verify other account balances
// Also verify the accounting equation
// ...
}
The Benefits of Our Collection-Based Approach
Our redesigned MockAccountBalanceCalculator offers several advantages:
Data-Driven: All calculations are based on collections of data, not hardcoded values.
Flexible: New transactions can be added easily without changing calculation logic.
Maintainable: If transaction amounts change, we only need to update them in one place.
Realistic: This approach closely mirrors how a real database-backed accounting system would work.
Extensible: We can add support for more complex queries like filtering by date range.
The Goals of Our Integration Test
Our integration test serves several important purposes:
Verify Module Integration: Ensures that the Document module and Chart of Accounts module work correctly together.
Validate Business Workflows: Confirms that standard accounting workflows (purchasing, sales, expenses, payments) function as expected.
Ensure Data Integrity: Verifies that all transactions maintain balance (debits = credits) and that account balances are accurate.
Test Double-Entry Accounting: Confirms that our system properly implements double-entry accounting principles where every transaction affects at least two accounts.
Validate Accounting Equation: Ensures that the fundamental accounting equation (Assets = Liabilities + Equity + (Revenues – Expenses)) remains balanced.
Conclusion
This integration test demonstrates the core functionality of our accounting system using a data-driven approach that closely mimics a real database. By simulating a retail business’s transactions and storing them in collections, we’ve created a realistic test environment for our double-entry accounting system.
The collection-based approach in our MockAccountBalanceCalculator allows us to test complex accounting logic without an actual database, while still ensuring that our calculations are accurate and our accounting principles are sound.
While this test uses in-memory collections rather than a database, it provides a strong foundation for testing the business logic of our accounting system in a way that would translate easily to a real-world implementation.
This call/zoom will give you the opportunity to define the roadblocks in your current XAF solution. We can talk about performance, deployment or custom implementations. Together we will review you pain points and leave you with recommendations to get your app back in track
The chart of accounts module is a critical component of any financial accounting system, serving as the organizational structure that categorizes financial transactions. As a software developer working on accounting applications, understanding how to properly implement a chart of accounts module is essential for creating robust and effective financial management solutions.
What is a Chart of Accounts?
Before diving into the implementation details, let’s clarify what a chart of accounts is. In accounting, the chart of accounts is a structured list of all accounts used by an organization to record financial transactions. These accounts are categorized by type (assets, liabilities, equity, revenue, and expenses) and typically follow a numbering system to facilitate organization and reporting.
Core Components of a Chart of Accounts Module
Based on best practices in financial software development, a well-designed chart of accounts module should include:
1. Account Entity
The fundamental entity in the module is the account itself. A properly designed account entity should include:
A unique identifier (typically a GUID in modern systems)
Account name
Account type (asset, liability, equity, revenue, expense)
Official account code (often used for regulatory reporting)
Reference to financial statement lines
Audit information (who created/modified the account and when)
Archiving capability (for soft deletion)
2. Account Type Enumeration
Account types are typically implemented as an enumeration:
This enumeration serves as more than just a label—it determines critical business logic, such as whether an account normally has a debit or credit balance.
3. Account Validation
A robust chart of accounts module includes validation logic for accounts:
Ensuring account codes follow the required format (typically numeric)
Verifying that account codes align with their account types (e.g., asset accounts starting with “1”)
Validating consistency between account types and financial statement lines
Checking that account names are not empty and are unique
4. Balance Calculation
One of the most important functions of the chart of accounts module is calculating account balances:
Point-in-time balance calculations (as of a specific date)
Period turnover calculations (debit and credit movement within a date range)
Determining if an account has any transactions
Implementation Best Practices
When implementing a chart of accounts module, consider these best practices:
1. Use Interface-Based Design
Implement interfaces like IAccount to define the contract for account entities:
When working with a chart of accounts module, you might encounter:
1. Account Code Standardization
Challenge: Different jurisdictions may have different account coding requirements.
Solution: Implement a flexible validation system that can be configured for different accounting standards.
2. Balance Calculation Performance
Challenge: Balance calculations for accounts with many transactions can be slow.
Solution: Implement caching strategies and consider storing period-end balances for faster reporting.
3. Account Hierarchies
Challenge: Supporting account hierarchies for reporting.
Solution: Implement a nested set model or closure table for efficient hierarchy querying.
Conclusion
A well-designed chart of accounts module is the foundation of a reliable accounting system. By following these implementation guidelines and understanding the core concepts, you can create a flexible, maintainable, and powerful chart of accounts that will serve as the backbone of your financial accounting application.
Remember that the chart of accounts is not just a technical construct—it should reflect the business needs and reporting requirements of the organization using the system. Taking time to properly design this module will pay dividends throughout the life of your application.
This call/zoom will give you the opportunity to define the roadblocks in your current XAF solution. We can talk about performance, deployment or custom implementations. Together we will review you pain points and leave you with recommendations to get your app back in track
It’s been a week since the Microsoft MVP Summit, and now I finally sit at Javier’s home trying to write about my trip and experience there. So let’s start!
The Journey
First, I needed to fly via Istanbul. That meant waking up around 2:00 AM to go to the airport and catch my flight at 6:00 AM. In Istanbul, I was really lucky because I was in the new airport which is huge and it has a great business lounge to wait in, so I could get some rest between my flights from Istanbul to Seattle.
I tried to sleep a little. The main problem was that the business lounge was on one side of the airport and my gate was on the other side, about 1 kilometer away. It’s a really big airport! I had to walk all that distance, and they announced the gate really late, so I only had about 15 minutes to get there—a really short time.
After that, I took my flight to the States, from Istanbul to Seattle. The route goes through the Arctic (near the North Pole)—you go up and then a little bit to the right, and then you end up in Seattle. It was a strange route; I’d never used it before. The flight was long, around 15 hours, but it wasn’t bad. I enjoyed Turkish Airlines when they use the big airplanes.
Arrival Challenges
I landed in Seattle around 6:00 PM. Then I had to go through immigration control and collect my luggage, which took almost two hours. After that, I went to the Airbnb, which was super beautiful, but I couldn’t get in because the owners had left the gate closed from the inside, and there were no lights at all, so it was impossible to enter. I waited for two hours for Javier to contact them, and after a while, it started raining, so I decided to go to a hotel. I booked a hotel for the night and took a 30-minute taxi ride. I finally went to bed on Monday at 11:00 PM, which was really late.
Day One at the Summit
The next day, I needed to drop my bags at the Airbnb and go to the MVP Summit. It was a nice experience. Javier was flying in that day and arrived around 3:00 PM, so I went to the first part alone. I missed the keynote because I had to drop off my bags and do all that stuff, so I ended up arriving around 11:00 AM.
The first person I met was Veronica, and we talked for a bit. Then I went to one of the sessions—of course, it was a Copilot session. In the afternoon, I met up with Javier, we grabbed some swag, and went to the Hub. Then I met Pablo from Argentina, and by the end of the day, I got together with Michael Washington, who I always hang out with during the MVP Summits.
Time to go home—it was a long day. We went back to the Airbnb, but didn’t do much. We just watched a TV show that our friend Hector recommended on Netflix.
Day Two: Meeting Peers
For day two, the sessions were great, but what I recall most are my meetings with specific people. When you go to the MVP Summits, you get to meet your peers. Usually, it’s like you’re good at one thing—for example, Javier and I do AI courses, and most of what we write about is general development—but there are people who really specialize.
For instance, I met the people from the Uno team, amazing people. Jerome and his team are always on the bleeding edge of .NET. We talked about the “black magic” they’ve written for their multi-target single application for Uno. It’s always nice to meet the Uno team.
I met with Michael Washington again several times in the hallways of Microsoft, and we talked about how to redirect Microsoft AI extensions to use LLM Studio, which is kind of tricky. It’s not something you can do really easily, like with Semantic Kernel where you only need to replace the HTTP client and then you’re good to go. In LLM Studio, it’s a different trick, so I’ll write about it later.
In one of the sessions, Mads Kristensen sat by my side, and I was trying to get some information from him on how to create an extension. Long ago, there was an extension from Oliver Sturm called “Instant Program Gratification” or something similar that displayed a huge congratulation message on the screen every time your compile succeeded, and if it failed, it would display something like “Hey, you need more coffee!” on the screen. I asked Mads how to achieve that with the new extension toolkit, and he explained it to me—he’s the king of extensions for Visual Studio.
Then I met someone new, Jeremy Sinclair, whom Javier introduced me to. We had one of those deep technical conversations about how Windows runs on ARM CPUs and the problems this can bring or how easy some things can be. It’s ironic because the Android architecture is usually ARM, but it doesn’t run on ARM computers because ARM computers emulate x64. We talked a lot about the challenges you might encounter and how to address them. Jeremy has managed to do it; he’s written some articles about what to expect when moving to an ARM computer. He also talked about how the future and the present for MAUI is at the moment.
He was also wearing the Ray-Ban Meta glasses, and I asked him, “Hey, how are they?” He told me they’re nice, though the battery life isn’t great, but they’re kind of fun. So I ordered a pair of Meta Ray-Ban AI glasses, and I like them so far.
More Memorable Conversations
Another great conversation that we had with Javier was with James Montemagno. We met him in the Hub, and then we talked a lot about how we started. I’ve been a long-term fan of Merge Conflict, their podcast, and Javier introduced me to that podcast a long time ago when we met around 9 years ago. When he was traveling to work, he called me, we talked mostly about development for about one hour on his way to work, and then he told me, “Hey, I listen to this and this podcast, I listen to that and that podcast.” So I became a follower of Merge Conflict after that.
James explained all the adventures on the Xamarin team, how it went when Xamarin joined Microsoft, about the difference between Xamarin from Microsoft and Xamarin from Xamarin Forms, and how life is changing for him as more of a project manager than an advocate. So he’s kind of busy all the time, but we had this really long conversation, like 40 minutes or so. He was really open about talking about his adventure of joining Microsoft and eventually working in the MAUI team.
We also met David from the MAUI team, and he was so nice. Long time ago, he featured our company in the list of companies that have made apps with MAUI, and we were on the list they showed in one of the conferences. So we thanked him for that.
That’s everyone I met at the MVP Summit. I had a great time, and I can’t believe it’s been a year already. I’m looking forward to meeting everyone next year and seeing what we come up with during 2025!