This guide will help you understand .NET assemblies and access modifiers by building a multi-project solution from scratch.
- How assemblies work in .NET
- Difference between
public
,internal
, andprivate
access modifiers - How to reference assemblies and use their public APIs
- Why
internal class Program
is used in modern .NET
- .NET 6 or later installed
- Command line or terminal access
# Create a new directory for our solution
mkdir AssemblyDemo
cd AssemblyDemo
# Create a solution file
dotnet new sln -n AssemblyDemo
# Create the main console app
dotnet new console -n MyApp
cd MyApp
# Look at the generated Program.cs - notice it uses 'internal class Program'
cat Program.cs
cd ..
# Add the project to solution
dotnet sln add MyApp/MyApp.csproj
# Create a class library
dotnet new classlib -n MyLibrary
cd MyLibrary
# Remove the default Class1.cs
rm Class1.cs
cd ..
# Add the library to solution
dotnet sln add MyLibrary/MyLibrary.csproj
Create MyLibrary/PublicUtility.cs
:
namespace MyLibrary
{
public class PublicUtility
{
public void PublicMethod()
{
Console.WriteLine("π’ This is a PUBLIC method from MyLibrary assembly");
// This works - calling internal method within same assembly
InternalMethod();
}
internal void InternalMethod()
{
Console.WriteLine("π This is an INTERNAL method (only visible within MyLibrary)");
}
private void PrivateMethod()
{
Console.WriteLine("π« This is a PRIVATE method (only visible within this class)");
}
}
}
Create MyLibrary/InternalUtility.cs
:
namespace MyLibrary
{
internal class InternalUtility
{
public void SomeMethod()
{
Console.WriteLine("π This is from an INTERNAL class - not accessible outside assembly");
}
}
}
Create MyLibrary/DatabaseHelper.cs
:
namespace MyLibrary
{
public class DatabaseHelper
{
public string ConnectionString { get; set; } = "DefaultConnection";
public void Connect()
{
Console.WriteLine($"π Connecting to database: {ConnectionString}");
}
internal void InternalCleanup()
{
Console.WriteLine("π§Ή Internal cleanup method");
}
}
}
# Add project reference
dotnet add MyApp/MyApp.csproj reference MyLibrary/MyLibrary.csproj
Replace MyApp/Program.cs
with:
using MyLibrary;
internal class Program // Notice: internal - not accessible from other assemblies
{
static void Main(string[] args)
{
Console.WriteLine("=== Assembly Demo ===");
Console.WriteLine("MyApp.exe assembly can use PUBLIC classes from MyLibrary.dll");
Console.WriteLine();
// β
This works - PublicUtility is public
var utility = new PublicUtility();
utility.PublicMethod();
// β This would NOT compile - InternalMethod is internal to MyLibrary
// utility.InternalMethod(); // Uncomment to see compilation error
Console.WriteLine();
// β
This works - DatabaseHelper is public
var dbHelper = new DatabaseHelper();
dbHelper.ConnectionString = "Server=localhost;Database=TestDB";
dbHelper.Connect();
// β This would NOT compile - InternalCleanup is internal
// dbHelper.InternalCleanup(); // Uncomment to see compilation error
Console.WriteLine();
// β This would NOT compile - InternalUtility is internal to MyLibrary
// var internalUtil = new InternalUtility(); // Uncomment to see compilation error
Console.WriteLine("β
Demo completed successfully!");
}
}
# Build the entire solution
dotnet build
# Run the main application using dotnet
dotnet run --project MyApp
# OR run the executable directly (after building)
./MyApp/bin/Debug/net8.0/MyApp
Note: The build process creates:
MyApp
- The main executable fileMyApp.dll
- The managed assembly containing your codeMyLibrary.dll
- The referenced library assembly (copied to output)
-
Try breaking the rules:
- Uncomment the commented lines in
Program.cs
- Try to build:
dotnet build
- Observe the compilation errors
- Uncomment the commented lines in
-
Check the generated assemblies:
# After building, check the output ls MyApp/bin/Debug/net*/ ls MyLibrary/bin/Debug/net*/
-
Test assembly dependencies:
# First, verify the app runs normally ./MyApp/bin/Debug/net8.0/MyApp # Now move the dependency and see what happens mv MyApp/bin/Debug/net8.0/MyLibrary.dll /tmp/ ./MyApp/bin/Debug/net8.0/MyApp # You'll see an error like: # System.IO.FileNotFoundException: Could not load file or assembly 'MyLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' # Restore the dependency mv /tmp/MyLibrary.dll MyApp/bin/Debug/net8.0/ ./MyApp/bin/Debug/net8.0/MyApp # Works again!
-
Make InternalUtility public:
- Change
internal class InternalUtility
topublic class InternalUtility
- Rebuild and see how this affects accessibility
- Change
Expected Output:
=== Assembly Demo ===
MyApp.exe assembly can use PUBLIC classes from MyLibrary.dll
π’ This is a PUBLIC method from MyLibrary assembly
π This is an INTERNAL method (only visible within MyLibrary)
π Connecting to database: Server=localhost;Database=TestDB
β
Demo completed successfully!
MyApp.exe
= One assemblyMyLibrary.dll
= Another assembly- Each has its own access control boundaries
Modifier | Visibility |
---|---|
public |
Accessible from any assembly |
internal |
Only accessible within the same assembly |
private |
Only accessible within the same class |
- Program class is an application entry point
- No external assembly should instantiate it
- Follows principle of least privilege
- Modern .NET templates use this by default
- Create a third project that references both MyApp and MyLibrary
- Try to access Program class from the new project (it won't work!)
- Create public static methods in Program class and see the difference
- Add XML documentation to understand what gets exposed in IntelliSense
# Remove the demo when done
cd ..
rm -rf AssemblyDemo
π Congratulations! You now understand how .NET assemblies and access modifiers work in practice!