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.
Related
According to the documentation of SimpleInjector one can use the following code to load assemblies dynamically
string pluginDirectory =
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins");
var pluginAssemblies =
from file in new DirectoryInfo(pluginDirectory).GetFiles()
where file.Extension.ToLower() == ".dll"
select Assembly.Load(AssemblyName.GetAssemblyName(file.FullName));
container.Collection.Register<IPlugin>(pluginAssemblies);
Let's say each plugin has its properties (name, language, ...) which are specifed in App.Config in the following manner
<Plugins>
<Element name="SamplePlugin" filename="SamplePlugin.dll" language="pl-PL" modeId="1" />
</Plugins>
These values are necessary as they are used in constructor of every plugin.
Currently there is a PluginSettings class which gets ConfigurationElements looping through a ConfigurationElementCollection and later on in the main ViewModel there is a loop going through the collection contained in the mentioned settings.
public class PluginSettings
{
readonly Configuration _config = ConfigurationManager
.OpenExeConfiguration(ConfigurationUserLevel.None);
public LoaderSection PluginAppearanceConfiguration
{
get
{
return (LoaderSection)_config.GetSection("pluginSection");
}
}
public PluginsCollection PluginsCollection
{
get
{
return PluginAppearanceConfiguration.PluginElement;
}
}
public IEnumerable<PluginElement> PluginElements
{
get
{
foreach (PluginElement selement in PluginsCollection)
{
if (selement != null)
yield return selement;
}
}
}
}
Current loading plugins in ViewModel
try
{
var factory = ModuleLoader
.LoadPluginFactory<PluginFactoryBase>(Path.Combine(pluginsPath, plug.Filename));
var configAppSettings = new ConfigAppSettings(plug.FactoryId, plug.ModeId, plug.Language);
Application.Current.Dispatcher.BeginInvoke((Action)delegate
{
plugins.Add(new Plugin(plug.Name, factory,
configSettings, configAppSettings));
});
}
catch (ReflectionTypeLoadException ex)
{
foreach (var item in ex.LoaderExceptions)
{
MessageBox.Show(item.Message, "Loader exception",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
Each plugin has its own PluginFactory defined. Code is a few years old and wasn't written by anyone still present. Further refactoring would include making common Factory in the main application as I don't see reasons for the current state. Plugins are listed in the main app and are initialized (by plugins PluginFactory) only when the specific one is selected (clicked) and then displayed.
How to adapt the code to follow good etiquette and process plugins in composition root and inject them?
This question is hard to answer, because it depends on what the ViewModel needs to do with this information.
When it comes to Simple Injector, however, all plugins that you might use during the execution of your application need to be registered up front. So, that means that if the ViewModel decides to only use 2 out of 5 defined plugins, you still need to register all 5. (* see note below)
But it likely means you need to do a few things:
Load the plugin information from the config
Use this plugin information to register the plugins in a collection in the order they are listed
Supply the plugin information and an IEnumerable<IPlugin> to the ViewModel.
Let the ViewModel pick the proper plugin from the collection based on the plugin information and the runtime data given at that time
For instance, if the ViewModel requires the plugin with language pl-PL, it needs to find out what the index is of the pl-PL plugin from the plugin information and use that index to get that plugin from the IEnumerable<IPlugin>, e.g.:
var info = pluginInformation.First(p => p.Language == "pl-PL");
int index = pluginInformation.IndexOf(info);
IPlugin plugin = plugins.ElementAt(index);
plugin.DoAwesomeStuff();
Note that with Simple Injector, a call to ElementAt on an injected IEnumerable<T> will be an optimized O(1) operation. So even if the collection contains hundreds of plugins, only one plugin will be created at that time.
*This isn't completely true, because you could load and register plugins just-in-time, but this is an advanced topic which you likely don't need.
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'm currently trying to load and use the Gephi Toolkit from within a .Net 4 C# website.
I have a version of the toolkit jar file compiled against the IKVM virtual machine, which works as expected from a command line application using the following code:
var controller = (ProjectController)Lookup.getDefault().lookup(typeof(ProjectController));
controller.closeCurrentProject();
controller.newProject();
var project = controller.getCurrentProject();
var workspace = controller.getCurrentWorkspace();
The three instances are correctly instantiated in a form similar to org.gephi.project.impl.ProjectControllerImpl#8ddb93.
If however I run the exact same code, with the exact same using statements & references, the very first line loading the ProjectController instance returns null.
I have tried a couple of solutions
Firstly, I have tried ignoring the Lookup.getDefault().lookup(type) call, instead trying to create my own instances:
var controller = new ProjectControllerImpl();
controller.closeCurrentProject();
controller.newProject();
var project = controller.getCurrentProject();
var workspace = controller.getCurrentWorkspace();
This fails at the line controller.newProject();, I think because internally (using reflector) the same Lookup.getDefault().lookup(type) is used in a constructor, returns null and then throws an exception.
Secondly, from here: Lookup in Jython (and Gephi) I have tried to set the %CLASSPATH% to the location of both the toolkit JAR and DLL files.
Is there a reason why the Lookup.getDefault().lookup(type) would not work in a web environment? I'm not a Java developer, so I am a bit out of my depth with the Java side of this.
I would have thought it possible to create all of the instances myself, but haven't been able to find a way to do so.
I also cannot find a way of seeing why the ProjectController load returned null. No exception is thrown, and unless I'm being very dumb, there doesn't appear to be a method to see the result of the attempted load.
Update - Answer
Based on the answer from Jeroen Frijters, I resolved the issue like this:
public class Global : System.Web.HttpApplication
{
public Global()
{
var assembly = Assembly.LoadFrom(Path.Combine(root, "gephi-toolkit.dll"));
var acl = new AssemblyClassLoader(assembly);
java.lang.Thread.currentThread().setContextClassLoader(new MySystemClassLoader(acl));
}
}
internal class MySystemClassLoader : ClassLoader
{
public MySystemClassLoader(ClassLoader parent)
: base(new AppDomainAssemblyClassLoader(typeof(MySystemClassLoader).Assembly))
{ }
}
The code ikvm.runtime.Startup.addBootClassPathAssemby() didn't seem to work for me, but from the provided link, I was able to find a solution that seems to work in all instances.
This is a Java class loader issue. In a command line app your main executable functions as the system class loader and knows how to load assembly dependencies, but in a web process there is no main executable so that system class loader doesn't know how to load anything useful.
One of the solutions is to call ikvm.runtime.Startup.addBootClassPathAssemby() to add the relevant assemblies to the boot class loader.
For more on IKVM class loading issues see http://sourceforge.net/apps/mediawiki/ikvm/index.php?title=ClassLoader
I'm looking for a method that let's me validate code and generator code as part of the build process, using Visual Studio 2010 (not express) and MSBuild.
Background Validation:
I'm writing a RESTful web service using the WCF Web Api. Inside the service class that represents the web service I have to define an endpoint, declaring additionally parameters as plain test. When the parameter name inside the endpoint declaration differs from the parameter of the C# method I get a error - unfortunately at run time when accessing the web service, not at compile time. So I thought it would be nice to analyze the web service class as part of the compile step for flaws like this, returning an error when something is not right.
Example:
[WebGet(UriTemplate = "Endpoint/{param1}/{param2}")]
public string MyMethod(string param1, string parameter2) {
// Accessing the web service now will result in an error,
// as there's no fitting method-parameter named "param2".
}
Also I'd like to enforce some naming rules, such as GET-Methods must start with the "Get" word. I believe this will help the service to remain much more maintainable when working with several colleagues.
Background Generation:
I will be using this REST web service in a few other projects, there for I need to write a client to access this service. But I don't want to write a client for each of these, always adjusting whenever the service changes. I'd like the clients to be generated automatically, based upon the web service code files.
Previous approach:
So far I tried to use a T4 template using the DTE interface to parse the code file and validate it, or generate the client. This worked fine in Visual Studio when saving manually, but integrating this in the build process turned out to be not so working well, as the Visual Studio host is not available using MSBuild.
Any suggestion is welcome. :)
Instead of using DTE or some other means to parse the C# code you could use reflection (with Reflection-Only context) to examine the assembly after it's compiled. Using reflection is a more robust solution and probably faster also (especially if you use Mono.Cecil to do the reflecting).
For the MSBuild integration I would recommend writing a custom MSBuild task - it's fairly easy and more robust/elegant than writing a command line utility that's executed by MSBuild.
This may be a long shot but still qualifies as "any suggestion" :)
You could compile the code, then run a post-build command which would be a tool that you'd have to write which uses reflection to compare the parsed UriTemplate text with the method parameter names, catching errors and outputting them in a manner that MSBuild will pickup. Look at This Link for information on how to output so MSBuild will put the errors in the visual studio error list. The post-build tool could then delete the compiled assemblies if errors were found, thus "simulating" a failed build.
Here's the SO Link that lead me to the MSBuild Blog too, just for reference.
HTH
For the enforcement side of things, custom FxCop rules would probably be a very good fit.
For the client code generation, there are quite a few possibilities. If you like the T4 approach, there is probably a way to get it working with MSBuild (but you would definitely need to provide a bit more detail regarding what isn't working now). If you're want an alternative anyway, a reflection-based post-build tool is yet another way to go...
Here is a short, extremely ugly program that you can run over an assembly or group of assemblies (just pass the dlls as arguments) to perform the WebGet UriTemplate check. If you don't pass anything, it runs on itself (and fails, appropriately, as it is its own unit test).
The program will print out to stdout the name of the methods that are missing the parameters and the names of the missing parameters, and if any are found, will return a non-zero return code (standard for a program failing), making it suitable as a post-build event. I am not responsible if your eyes bleed:
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.ServiceModel.Web;
namespace ConsoleApplication1
{
class Program
{
static int Main(string[] args)
{
var failList = new ConcurrentDictionary<MethodInfo, ISet<String>>();
var assembliesToRunOn = (args.Length == 0 ? new[] {Assembly.GetExecutingAssembly()} : args.Select(Assembly.LoadFrom)).ToList();
assembliesToRunOn.AsParallel().ForAll(
a => Array.ForEach(a.GetTypes(), t => Array.ForEach(t.GetMethods(BindingFlags.Public | BindingFlags.Instance),
mi =>
{
var miParams = mi.GetParameters();
var attribs = mi.GetCustomAttributes(typeof (WebGetAttribute), true);
if (attribs.Length <= 0) return;
var wga = (WebGetAttribute)attribs[0];
wga.UriTemplate
.Split('/')
.ToList()
.ForEach(tp =>
{
if (tp.StartsWith("{") && tp.EndsWith("}"))
{
var tpName = tp.Substring(1, tp.Length - 2);
if (!miParams.Any(pi => pi.Name == tpName))
{
failList.AddOrUpdate(mi, new HashSet<string> {tpName}, (miv, l) =>
{
l.Add(tpName);
return l;
});
}
}
});
})));
if (failList.Count == 0) return 0;
failList.ToList().ForEach(kvp => Console.Out.WriteLine("Method " + kvp.Key + " in type " + kvp.Key.DeclaringType + " is missing the following expected parameters: " + String.Join(", ", kvp.Value.ToArray())));
return failList.Count;
}
[WebGet(UriTemplate = "Endpoint/{param1}/{param2}")]
public void WillPass(String param1, String param2) { }
[WebGet(UriTemplate = "Endpoint/{param1}/{param2}")]
public void WillFail() { }
[WebGet(UriTemplate = "Endpoint/{param1}/{param2}")]
public void WillFail2(String param1) { }
}
}
I have a requirement to install multiple web setup projects (using VS2005 and ASP.Net/C#) into the same virtual folder. The projects share some assembly references (the file systems are all structured to use the same 'bin' folder), making deployment of changes to those assemblies problematic since the MS installer will only overwrite assemblies if the currently installed version is older than the one in the MSI.
I'm not suggesting that the pessimistic installation scheme is wrong - only that it creates a problem in the environment I've been given to work with. Since there are a sizable number of common assemblies and a significant number of developers who might change a common assembly but forget to update its version number, trying to manage versioning manually will eventually lead to massive confusion at install time.
On the flip side of this issue, it's also important not to spontaneously update version numbers and replace all common assemblies with every install, since that could (temporarily at least) obscure cases where actual changes were made.
That said, what I'm looking for is a means to update assembly version information (preferably using MSBuild) only in cases where the assembly constituents (code modules, resources etc) has/have actually changed.
I've found a few references that are at least partially pertinent here (AssemblyInfo task on MSDN) and here (looks similar to what I need, but more than two years old and without a clear solution).
My team also uses TFS version control, so an automated solution should probably include a means by which the AssebmlyInfo can be checked out/in during the build.
Any help would be much appreciated.
Thanks in advance.
I cannot answer all your questions, as I don't have experience with TFS.
But I can recommend a better approach to use for updating your AssemblyInfo.cs files than using the AssemblyInfo task. That task appears to just recreate a standard AssemblyInfo file from scratch, and loses any custom portions you may have added.
For that reason, I suggest you look into the FileUpdate task, from the MSBuild Community Tasks project. It can look for specific content in a file and replace it, like this:
<FileUpdate
Files="$(WebDir)\Properties\AssemblyInfo.cs"
Regex="(\d+)\.(\d+)\.(\d+)\.(\d+)"
ReplacementText="$(Major).$(ServicePack).$(Build).$(Revision)"
Condition="'$(Configuration)' == 'Release'"
/>
There are several ways you can control the incrementing of the build number. Because I only want the build number to increment if the build is completely successful, I use a 2-step method:
read a number from a text file (the only thing in the file is the number) and add 1 without changing the file;
as a final step in the build process, if everything succeeded, save the incremented number back to the text file.
There are tasks such as ReadLinesFromFile, that can help you with this, but I found it easiest to write a small custom task:
using System;
using System.IO;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
namespace CredibleCustomBuildTasks
{
public class IncrementTask : Task
{
[Required]
public bool SaveChange { get; set; }
[Required]
public string IncrementFileName { get; set; }
[Output]
public int Increment { get; set; }
public override bool Execute()
{
if (File.Exists(IncrementFileName))
{
string lines = File.ReadAllText(IncrementFileName);
int result;
if(Int32.TryParse(lines, out result))
{
Increment = result + 1;
}
else
{
Log.LogError("Unable to parse integer in '{0}' (contents of {1})");
return false;
}
}
else
{
Increment = 1;
}
if (SaveChange)
{
File.Delete(IncrementFileName);
File.WriteAllText(IncrementFileName, Increment.ToString());
}
return true;
}
}
}
I use this before the FileUpdateTask to get the next build number:
<IncrementTask
IncrementFileName="$(BuildNumberFile)"
SaveChange="false">
<Output TaskParameter="Increment" PropertyName="Build" />
</IncrementTask>
and as my final step (before notifying others) in the build:
<IncrementTask
IncrementFileName="$(BuildNumberFile)"
SaveChange="true"
Condition="'$(Configuration)' == 'Release'" />
Your other question of how to update the version number only when source code has changed is highly dependent on your how your build process interacts with your source control. Normally, checking in source file changes should initiate a Continuous Integration build. That is the one to use to update the relevant version number.
I have written one custome task you can refer the code below. It will create an utility to which you can pass assemblyinfo path Major,minor and build number. you can modify it to get revision number. Since in my case this task was done by developer i used to search it and again replace whole string.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Text.RegularExpressions;
namespace UpdateVersion
{
class SetVersion
{
static void Main(string[] args)
{
String FilePath = args[0];
String MajVersion=args[1];
String MinVersion = args[2];
String BuildNumber = args[3];
string RevisionNumber = null;
StreamReader Reader = File.OpenText(FilePath);
string contents = Reader.ReadToEnd();
Reader.Close();
MatchCollection match = Regex.Matches(contents, #"\[assembly: AssemblyVersion\("".*""\)\]", RegexOptions.IgnoreCase);
if (match[0].Value != null)
{
string strRevisionNumber = match[0].Value;
RevisionNumber = strRevisionNumber.Substring(strRevisionNumber.LastIndexOf(".") + 1, (strRevisionNumber.LastIndexOf("\"")-1) - strRevisionNumber.LastIndexOf("."));
String replaceWithText = String.Format("[assembly: AssemblyVersion(\"{0}.{1}.{2}.{3}\")]", MajVersion, MinVersion, BuildNumber, RevisionNumber);
string newText = Regex.Replace(contents, #"\[assembly: AssemblyVersion\("".*""\)\]", replaceWithText);
StreamWriter writer = new StreamWriter(FilePath, false);
writer.Write(newText);
writer.Close();
}
else
{
Console.WriteLine("No matching values found");
}
}
}
}
I hate to say this but it seems that you may be doing it wrongly. Is much easier if you do generate the assembly versions on the fly instead of trying to patch them.
Take a look at https://sbarnea.com/articles/easy-windows-build-versioning/
Why I do think you are doing it wrong?
* A build should not modify the version number
* if you build the same changeset twice you should get the same build numbers
* if you put build number inside what microsoft calls build number (proper naming would be PATCH level) you will eventually reach the 65535 limitation.