I'm writing a class that I wish to use on both Windows & Linux.
One of the methods that is in this class is accessing the Windows Registry
What I'm hoping to achieve is to somehow disable this particular method from being used when using a Linux machine.First of all I did some research to see if there was something for .Net Core that would allow me to check which operating system is in use, I found this and sure enough that works.When I implemented it into my code when accessing a method, I was hoping to disable the method that's accessing the windows registry, however the closest I could get to this was using a switch statement, something like this
switch (OS)
{
case OSX:
return;
case LINUX:
return
}
To return if the operating system was not supported, this worked, however I then thought disabling it from being accessed all together would be much better rather than throwing an error for an operating system thats not supported for that particular methodI then went on to look at preprocessor directives thinking that if I'm able to detect and disable parts of code depending the frameworks etc, maybe I could use something like this to disable parts of code depending on the operating system that way they could never be called even when trying to access the methodI went on from there to see if I could disable parts of code using preprocessor directives.I found this.I understand that it is for C++ however it seems to be the closest I could find for what I'm trying to achieve within .Net Core In a perfect world, it would look something like this
/// <summary>
/// Get the file mime type
/// </summary>
/// <param name="filePathLocation">file path location</param>
/// <returns></returns>
`#if WINDOWS`
public static string GetMimeType(this string filePathLocation)
{
if (filePathLocation.IsValidFilePath())
{
string mimeType = "application/unknown";
string ext = Path.GetExtension(filePathLocation).ToLower();
Microsoft.Win32.RegistryKey regKey = Microsoft.Win32.Registry.ClassesRoot.OpenSubKey(ext);
if (regKey != null && regKey.GetValue("Content Type") != null)
{
mimeType = regKey.GetValue("Content Type").ToString();
}
return mimeType;
}
return null;
}
`#endif`
I did see #Define so I tried something like this #define IS_WINDOWS and added it to my class along with #if IS_WINDOWS
however, I couldn't see how to change that value if I'm hoping to just reuse the static class over and over.
While you could pursue a route involving #define, it's compile-time and you'll loose a lot of .Net's multi-platform goodness. You'll also have to juggle multiple configurations, multiple builds, etc.
Where possible, hide the platform-dependent behavior behind a platform-independent abstraction and do the check at runtime using System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform:
interface IPlatform
{
void DoSomething();
}
class WindowsImpl : IPlatform
{
public void DoSomething()
{
// Do something on Windows
}
}
class LinuxImpl : IPlatform
{
public void DoSomething()
{
// Do something on Linux
}
}
// Somewhere else
var platform = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? new WindowsImpl() : new LinuxImpl();
platform.DoSomething();
This works well for many things including PInvoke. You will be able to use the same binaries on either platform, and it will be easier to add OSX later.
If you need to isolate the platform-dependent code at compile-time (perhaps a package is Windows-only), MEF2/System.Composition can help you make a plugin framework where each platform gets its own assembly:
// In Windows.dll class library project
using System.Composition;
[Export(typeof(IPlatform))]
public class WindowsImpl : IPlatform
{
public void DoSomething()
{
//...
}
}
And then in your main program:
using System.Composition.Hosting;
var configuration = new ContainerConfiguration();
var asm = Assembly.LoadFrom(pathToWindowsDll);
configuration.WithAssembly(asm);
var host = configuration.CreateContainer();
var platform = host.GetExports<IPlatform>().FirstOrDefault();
I have a use-case where preprocessor directives are necessary. I found here that I can do this. As the site tells us, add the following to your project (.csproj) file:
<PropertyGroup>
<TargetFramework>...</TargetFramework>
<OutputType>...</OutputType>
<!-- insert the following -->
<IsWindows Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))' == 'true'">true</IsWindows>
<IsOSX Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::OSX)))' == 'true'">true</IsOSX>
<IsLinux Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))' == 'true'">true</IsLinux>
</PropertyGroup>
... then add these for each preprocessor option:
<PropertyGroup Condition="'$(IsWindows)'=='true'">
<DefineConstants>Windows</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(IsOSX)'=='true'">
<DefineConstants>OSX</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(IsLinux)'=='true'">
<DefineConstants>Linux</DefineConstants>
</PropertyGroup>
It is the individual <PropertyGroup Condition> child element that defines the constant so I can do this:
#if Linux
private const string GLFW_LIB = "glfw";
#elif OSX
private const string GLFW_LIB = "libglfw.3";
#elif Windows
private const string GLFW_LIB = "glfw3";
#else
// some error condition - unsupported platform
#endif
Related
Intro
I am looking for more customized solution for translating my app. I will be using Humanizer and Smart.Format after obtaining entries. The problem is to define keys to obtain them in the first place.
Requirements
The requirements are:
Language keys must be defined in-code, preferably near place where they are used
Language keys must contain default-English values
All language keys must be listed (XML, CSV, JSON, anything) after building the app suite
Language entries must be provided from external source (like JSON file), without the need for any kind of recompilation
The app may contain multiple executables, shared libraries, etc. all of them in form of C# apps
Discarded solutions
First, the things I discarded:
Built-in C# Resources.dll; They violate (1) and (4)
External file with keys. Violates (1)
My idea for handling the problem
Now, my idea for the solution looks that way (and is inspired by C++ GetText)
There is a template class which contains keys:
private sealed class Module1Keys : LocalizationKeys<Module1Keys>
{
public static readonly LocalizationKey HelloWorld = DefineKey("/foo", "Hello World!");
public static readonly LocalizationKey HelloWorld2 = DefineKey("/bar", "Hello World2!");
}
And the class LocalizationKeys contains a static method that will actually register keys in simple collection
public abstract class LocalizationKeys<T> where T : LocalizationKeys<T>
{
protected static LocalizationKey DefineKey(string path, string english)
{
var ret = new LocalizationKey(typeof(T), path, english);
// Following registers localization key in runtime:
Localization.Instance.RegisterLocalizableKey(ret);
return ret;
}
}
Problem
The only thing left to handle in this approach is to list localizable keys during build... which is where I had hit the wall. It is very easy to list them during runtime, but I cannot run the code on build time (particularly it may be built as shared library).
Maybe I am overthinking myself and there is better, more clean solution - I don't need to stick with this solution, but Googling around has not yielded anything better...
Nailed it. In GetText times we have to resort to manually parse code.
... but now, with CSharp, we have a Roslyn, with CodeAnalysis API.
Solution
Wire up custom Console build tool that includes Microsoft.CodeAnalysis NuGet and have code like:
var model = compilation.GetSemanticModel(tree);
var methods = root.DescendantNodes().OfType<InvocationExpressionSyntax>();
foreach(var method in methods)
{
if(model.GetSymbolInfo(method).Symbol is IMethodSymbol symbol &&
symbol.ContainingNamespace.Name == "MyProject" &&
symbol.ContainingType.Name == "LocalizationKeys" &&
symbol.Name == "DefineKey")
{
var key = method.ArgumentList.Arguments.FirstOrDefault();
var eng = method.ArgumentList.Arguments.Skip(1).FirstOrDefault();
if(key.Expression is LiteralExpressionSyntax literalKey &&
eng.Expression is LiteralExpressionSyntax literalEng)
{
// "/foo" -> "Hello World!"
// "/bar" -> "Hello World2!"
Console.WriteLine(literalKey + " -> " + literalEng);
}
else
{
// Bonus: detect violation of key definition rule. It is not a literal!
}
}
}
Compile this Console tool as executable and add it as post-build step. Profit.
my question does not target a problem. It is more some kind of "Do you know something that...?". All my applications are built and deployed using CI/CD with Azure DevOps. I like to have all build information handy in the create binary and to read them during runtime. Those applications are mainly .NET Core 2 applications written in C#. I am using the default build system MSBuild supplied with the .NET Core SDK. The project should be buildable on Windows AND Linux.
Information I need:
GitCommitHash: string
GitCommitMessage: string
GitBranch: string
CiBuildNumber: string (only when built via CI not locally)
IsCiBuild: bool (Detecting should work by checking for env variables
which are only available in CI builds)
Current approach:
In each project in the solution there is a class BuildConfig à la
public static class BuildConfig
{
public const string BuildNumber = "#{Build.BuildNumber}#"; // Das sind die Namen der Variablen innerhalb der CI
// and the remaining information...
}
Here tokens are used, which get replaced with the corresponding values during the CI build. To achieve this an addon task is used. Sadly this only fills the values for CI builds and not for the local ones. When running locally and requesting the build information it only contains the tokens as they are not replaced during the local build.
It would be cool to either have the BuildConfig.cs generated during the build or have the values of the variables set during the local build (IntelliSense would be very cool and would prevent some "BuildConfig class could not be found" errors). The values could be set by an MSBuild task (?). That would be one (or two) possibilities to solve this. Do you have ideas/experience regarding this? I did not found that much during my internet research. I only stumbled over this question which did not really help me as I have zero experience with MSBuild tasks/customization.
Then I decided to have a look at build systems in general. Namly Fake and Cake. Cake has a Git-Addin, but I did not find anything regarding code generation/manipulation. Do you know some resources on that?
So here's the thing...
Short time ago I had to work with Android apps namly Java and the build system gradle. So I wanted to inject the build information there too during the CI build. After a short time I found a (imo) better and more elegant solution to do this. And this was modifying the build script in the following way (Scripting language used is Groovy which is based on Java):
def getGitHash = { ->
def stdout = new ByteArrayOutputStream()
exec {
commandLine 'git', 'rev-parse', '--short', 'HEAD'
standardOutput = stdout
}
return stdout.toString().trim().replace("\"", "\\\"")
}
def getGitBranch = { ->
def fromEnv = System.getenv("BUILD_SOURCEBRANCH")
if (fromEnv) {
return fromEnv.substring("refs/heads/".length()).replace("\"", "\\\"");
} else {
def stdout = new ByteArrayOutputStream()
exec {
commandLine 'git', 'rev-parse', '--abbrev-ref', 'HEAD'
standardOutput = stdout
}
return stdout.toString().trim().replace("\"", "\\\"")
}
}
def getIsCI = { ->
return System.getenv("BUILD_BUILDNUMBER") != null;
}
# And the other functions working very similar
android {
# ...
buildConfigField "String", "GitHash", "\"${getGitHash()}\""
buildConfigField "String", "GitBranch", "\"${getGitBranch()}\""
buildConfigField "String", "BuildNumber", "\"${getBuildNumber()}\""
buildConfigField "String", "GitMessage", "\"${getGitCommitMessage()}\""
buildConfigField "boolean", "IsCIBuild", "${getIsCI()}"
# ...
}
The result after the first build is the following java code:
public final class BuildConfig {
// Some other fields generated by default
// Fields from default config.
public static final String BuildNumber = "Local Build";
public static final String GitBranch = "develop";
public static final String GitHash = "6c87e82";
public static final String GitMessage = "Merge branch 'hotfix/login-failed' into 'develop'";
public static final boolean IsCIBuild = false;
}
Getting the required information is done by the build script itself without depending on the CI engine to fulfill this task. This class can be used after the first build its generated and stored in a "hidden" directory which is included in code analysis but exluded from your code in the IDE and also not pushed to the Git. But there is IntelliSense support. In C# project this would be the obj/ folder I guess. It is very easy to access the information as they are a constant and static values (so no reflection or similar required).
So here the summarized question: "Do you know something to achieve this behaviour/mechanism in a .NET environment?"
Happy to discuss some ideas/approaches... :)
It becomes much easier if at runtime you are willing to use reflection to read assembly attribute values. For example:
using System.Reflection;
var assembly = Assembly.GetExecutingAssembly();
var descriptionAttribute = (AssemblyDescriptionAttribute)assembly
.GetCustomAttributes(typeof(AssemblyDescriptionAttribute), false).FirstOrDefault();
var description = descriptionAttribute?.Description;
For most purposes the performance impact of this approach can be satisfactorily addressed by caching the values so they only need to read once.
One way to embed the desired values into assembly attributes is to use the MSBuild WriteCodeFragment task to create a class file that sets assembly attributes to the values of project and/or environment variables. You would need to ensure that you do this in a Target that executes before before compilation occurs (e.g. <Target BeforeTargets="CoreCompile" ...). You would also need to set the property <GenerateAssemblyInfo>false</GenerateAssemblyInfo> to avoid conflicting with the functionality referenced in the next option.
Alternatively, you may be able to leverage the plumbing in the dotnet SDK for including metadata in assemblies. It embeds the values of many of the same project variables documented for the NuGet Pack target. As implied above, this would require the GenerateAssemblyInfo property to be set to true.
Finally, consider whether GitVersion would meet your needs.
Good luck!
I have a libary which needs to behave differently for console applications, desktop application (e.g. WPF), and for UWP apps.
How can I determine at run-time into which application type my libary is loaded?
Determining if it is a console application seems easy: How to tell if there is a console
For UWP, I can probably determine if WinRT is loaded. But how?
What distinguishing attributes do desktop applications have?
I ended up defining following enum:
public enum ExecutionMode
{
Console,
Desktop,
UniversalWindowsPlatform
}
which is passed to the constructor of the main class of my libary. Not a new idea, but very reliable (if used correctly).
Create a CustomAttribute in an assembly that is available to all of the applications like so
using System;
namespace MyNamespace.Reflection {
[System.AttributeUsage(AttributeTargets.Assembly)]
public class ApplicationTypeAttribute : Attribute {
public enum ApplicationTypes {
Console,
Desktop,
UWP,
ClassLibrary
}
public ApplicationTypeAttribute(ApplicationTypes appType) {
ApplicationType = appType;
}
public ApplicationTypes ApplicationType { get; private set; } = ApplicationTypes.Console;
}
}
Then add this attribute to your AssemblyInfo.cs file for a console application
using MyNamespace.Reflection;
[assembly: ApplicationType(ApplicationTypeAttribute.ApplicationTypes.Console)]
or a Desktop application
[assembly: ApplicationType(ApplicationTypeAttribute.ApplicationTypes.Desktop)]
etc.
Then wherever you want to check the calling type of the application that was started, use this
using MyNamespace.Reflection;
var assy = System.Relection.Assembly.GetEntryAssembly();
var typeAttribute = assy.GetCustomAttribute(typeof(ApplicationTypeAttribute));
if (typeAttribute != null) {
var appType = ((ApplicationTypeAttribute)typeAttribute).ApplicationType;
}
There is one caveat to this method. .NET Core apps have a different project structure and the AssemblyInfo.cs file is auto-generated at build time by default. You can override this behavior by specifying the following in the .csproj file in the Project node.
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
To match the old project file structure, you can create a Properties directory in the project directtory and then you can add an AssemblyInfo.cs file to that directory. Otherwise you can place the Custom Attribute definition in any file (after the usings and before the namespace declaration).
Is it possible to use platform specific code like so:
#if __ANDROID__
using Android.App;
using Android.Content;
using Android.Locations;
#endif
public GPSNavigation()
{
#if __ANDROID__
manager = (LocationManager)DependencyService.Get<Activity>().GetSystemService(Context.LocationService);
Criteria criteria = new Criteria
{
Accuracy = Accuracy.Fine
};
IList<string> acceptableLocationProviders = manager.GetProviders(criteria, true);
if (acceptableLocationProviders.Any())
{
locationProvider = acceptableLocationProviders.First();
}
else
{
locationProvider = "";
}
#endif
#if __IOS__
#endif
}
The code compiles and runs but I get a runtime error of "Unknown identifier: " for all Android specific code such as "Unknown identifier: LocationManager" when executed. Is it not possible to reference platform specific services/APIs within a shared project?
Yes, in answer to your question, this is possible. Shared projects get whatever references the project that references them have. Otherwise it wouldn't compile. You must have a logic error in your code.
From your code I suspect the value of Context.LocationService is LocationManager and the call to the DependencyService is not resolving it. Why this is occurring is not directly related to using a shared project, but something that needs to be debugged.
Additionally, based on your code you might be able to take advantage of #elif IOS instead of the separate #endif and #if IOS statements but that is more an organizational preference.
What I want to do is set different variable values depending on the current configuration. So when I run the project in Debug, I would like the app to point towards my development stack. When I run the app in Release I would like the app to run against my production stack. And I would like to create a Beta config which will be mostly the same as my Release config settings but slightly different.
Thanks!
Take a look at the Slow Cheetah extension for Visual Studio. It allows you to transform you config files based on the current build configuration.
Alternatively you could use Conditional Compilation Symbols. But I'd recommend the config transforms.
EDIT:
Seeing as you are unable to use the configuration transforms I think you're forced to go do the route of compilation symbols. So I'd create the configuration files and then have a class use compilation symbols to read the correct one at runtime. Something along the lines of:
public class ConfigurationLoader()
{
private readonly string _configFile;
public ConfigurationLoader()
{
#if DEBUG
_configFile = "app.Debug.config";
#else
_configFile = "app.Release.config";
#endif
}
public UniversalAppConfig LoadCofig()
{
// Read file
// Create UniversalAppConfig
}
}
public class UniversalAppConfig()
{
public int ConfigurationValueA { get; set; }
public int ConfigurationValueB { get; set; }
}
You set the symbols under Project Properties -> Build by the way.