For a more complete answer: you can use .NET/CLI as the lowest level bridge between C++ and .NET. It has some advantages to be able to interact with the Cpp code in a more convinient way but you need to take attention to the GC and it is Windows only since Mono dosen't support it.
The alternative is a cross-language layer or the so-called p/Invoke. You write your code in full featured C# and declare every function you want to use from your engine in C#. The CLR then lazy binds those functions at runtime if they are used for the first time. The disadvantage of this is however that you have to take attention how your function is defined in Cpp, parameter types, calling convention etc. because the CLR dosen't help you here or provide usefull information if something fails.
Also you have to take attention how you expose the functions you want to use. p/Invoke works with C-Style APIs only, it is very difficult to call a static class function for example. Also you should avoid namespaces because of the compiler name mengling problem in those cases.
What I did with my C# editor to Cpp engine code is to use p/Invoke but have a special exposure layer between the engine code and C#. It is written in pure C-Style and provides access to the engine features and functions. For example if I need access to an array of available devices, I have a function in my exposure layer
#define api_export __declspec(dllexport) cdecl
force_inline api_export void* GetDeviceList()
{
int16 numDevices = Graphics::GetDisplayDevices(nullptr);
Array<DisplayDevice> devices = *Allocator::Default::Allocate(numDevices);
Graphics::GetDisplayDevices(&devices);
return &devices;
}
So this way I manage the engine memory on the engine side while exposing IntPtr to the C# side. For more convinience, for example to access the native array, there are also exposure functions for Get/Set, Length etc.
Finally I wrapped those functions on the C# side into a bindings layer, a collection of classes that make use of the exposure functions but mimic to C# as if this were all OOP. I have a NativeArray, NativeAllocator that also does memory cleanup if it gets disposed etc.
My editor code in the end uses the bindings layer, for example to enumerate the display devices with native array encapsulated into some more convinient classes