C# Sending data using Memory-Mapped files
Sending data, be it a simple string or object(s) to a completely different application was easier than anticipated. While I was working on an application I actually encountered an instance where I was required to re-authenticate the user in a separate application, as I do not want to prompt the user to, once again sign in. I had several ideas of either storing the token on the user his system or why not create a static entry point and just overwrite the string from memory.
Apparently there was a much easier solution... Today we discuss MemoryMappedFile
Overview
MemoryMappedFile, as the name indicates allows for storage of a file that resides in the memory-verse 😉. This allows us to share data between multiple processes on the running machine.
There are two types of memory-mapped files, persisted and non-persisted. The persisted type contains a file on the running machine, this allows the consumer to retrieve data even when the application is no longer active and the garbage collector has run. The non-persisted type is the exact opposite, with the result of losing the data when the application is closed.
In this example we will be working with non-persisted memory-mapped file(s).
Setting up our solution
For this I created 3 projects, two simple console applications and one library.
- Producer
- The producer is the application which will instantiate the memory-mapped file
- Consumer
- The consumer is the receiver, his purpose is to interact with the newly given data
- Core
- A simple library that shares the memory-mapped methods and model
Creating the core library
We start working on the core first, this project will contain our logic and share it between the console applications.
MemoryMapper
Create a new class, we give it the name MemoryMapper.cs
I decided to just work with objects in this example, but feel free to experiment. I'll start off by requiring a class constraint, as we might eventually store more than just authentication tokens.
public sealed class MemoryMapper<T> where T : class {}
First we need to create a method which will create the non-persisted memory-mapped file. For this we will be using MemoryMappedFile and MemoryMappedViewStream.
Considering this is a non-persisted memory-mapped file we will be using the ViewStream, this is the recommended method. The ViewStream will be read in sequential access, aka read the file from start to finish in the order the objects were stored/assigned.
Creating/Writing to the memory-mapped file
/// <summary>
/// Store a object in an memory mapped file, non-persisted
/// </summary>
/// <param name="mapName">The name of the memory-mapped file</param>
/// <param name="entity">The model you want to serialize into a memory-mapped file</param>
/// <param name="capacitySize">The maximum size, in bytes, to allocate to the memory-mapped file</param>
public void CreateMemoryMappedFile(string mapName, T entity, long capacitySize)
{
try
{
// Creates a memory-mapped file with the specified name and size.
var mmf = MemoryMappedFile.CreateOrOpen(mapName, capacitySize);
// Creates a stream, this is meant for reading/writing to the mmf
// You can define an offset e.g. 0x1C, but keep in mind you need this offset when reading
// The length will remain at 0, this way we start at the offset and it ends at the approx end of the memory-mapped file
var mmvStream = mmf.CreateViewStream(0, 0);
// Uses System.Text.Json, this became available in .NET Core, you might need external libraries to achieve this in .NET FW
// Or if you are lazy, you could use the obsolete BinaryFormatter as well
using var jsonWriter = new Utf8JsonWriter(mmvStream);
JsonSerializer.Serialize(jsonWriter, entity);
}
}
catch (Exception e)
{
Debug.WriteLine(e.Message);
throw;
}
}
You could create multiple overloads to support different data types, instead of just objects, however to keep it simple and minimal, this should suffice more than enough.
In the same class, create another method to open the memory-mapped file.
Reading the memory-mapped file
/// <summary>
/// Tries to read content of the memory-mapped file
/// </summary>
/// <param name="mapName">The name of the memory-mapped file</param>
/// <returns>T Generic</returns>
public T ReadMemoryMappedFileToEntity(string mapName)
{
try
{
// We use the mapName to find the existing memory-mapped file
var mmf = MemoryMappedFile.OpenExisting(mapName);
var mmvStream = mmf.CreateViewStream(0, 0);
using var sr = new StreamReader(mmvStream);
var json = sr.ReadToEnd().Trim('\0');
return mmvStream.CanRead ? JsonSerializer.Deserialize<T>(json) : null;
}
catch (FileNotFoundException)
{
Debug.WriteLine("Memory-mapped file not found.");
throw;
}
}
Auth model
Create a new class; Auth.cs. We are using this to distribute our Bearer token to the consumer application.
[Serializable]
public class Auth
{
private const string memoryMappedFileName = "example";
public static string MemoryMappedFileName => memoryMappedFileName;
public string Bearer { get; set; }
public Auth(string bearer)
{
Bearer = bearer;
}
}
The Producer/Consumer
Assuming you already have the same the project structure, add the core as project reference to both the consumer and producer.
Open program.cs in the producer project. Initialize a new Auth class and fill in the constructor value. Now we can create a new instance of the MemoryMapper class. This class contains the method to create a new memory-mapped file, CreateMemoryMappedFile.
Producer
var auth = new Auth(bearerToken);
var memoryMapper = new MemoryMapper<Auth>();
// Memory-mapped file from object
memoryMapper.CreateMemoryMappedFile(Auth.MemoryMappedFileName, auth, 1000);
Open program.cs in the consumer project. As we won't be writing to the Auth class we can directly initialize the MemoryMapper class. This class contains another method we just created, ReadMemoryMappedFileToEntity.
Consumer
var memoryMappedAuth = new MemoryMapper<Auth>();
// Reads the memory-mapped file, returns object
var resultAuth = memoryMappedAuth.ReadMemoryMappedFileToEntity(Auth.MemoryMappedFileName);
// Reads the memory-mapped file, returns string
var resultString = memoryMappedAuth.ReadMemoryMappedFileToString(Auth.MemoryMappedFileName);
Console.WriteLine(resultAuth.Bearer);
Console.WriteLine(resultString);
Result
Once the producer and consumer are done, you are ready to build the application. If the application finished building, make sure you launch the producer first, this will create the memory-mapped file. As soon as the producer is active, you can start the consumer.