I manage my application-settings using the setting-designer in VS2008.
"The exact path of the user.config
files looks something like this:"
<Profile Directory>\<Company Name>\
<App Name>_<Evidence Type>_<Evidence Hash>\<Version>\user.config
Is there to a way to customize this path? I would prefer something like this:
<Profile Directory>\<Company Name>\
<App Name>\<Version>\user.config
I noticed that white-spaces were replaced by underscores in the "Company Name" in the new created folder ("Test Company" --> "Test_Company"). I really wish to turn off this behavior.
You know, I could write a new XML-based setting-handler, but I would like to use the setting-designer.
It hasn't been easy to find good info on implementing a custom settings provider so I'm including a complete implementation below (bottom.) The format of the user.config file is retained, as well as the functionality within the .settings designer. I'm certain there are parts that can be cleaned up a bit, so don't hassle me :)
Like others, I wanted to change the location of the user.config file and still get the fun and fanciness of working with the .settings files in the designer, including creating default values for new installations. Importantly, our app also already has other saved settings objects at a path (appData\local\etc) in which we have already decided, and we didn't want artifacts in multiple locations.
The code is much longer than I'd like it to be, but there is no SHORT answer that I could find. Though it seems somewhat painful just to be able to control the path, creating a custom settings provider in itself is still pretty powerful. One could alter the follwing implementation to store the data just about anywhere including a custom encrypted file, database, or interact with a web serivice.
From what I've read, Microsoft does not intend on making the path to the config file configurable. I'll take their word for it when they say allowing that would be scary. See (this) post. Alas, if you want to do it yourself you must implement your own SettingsProvider.
Here goes..
Add a reference in your project to System.Configuration, you'll need it to implement the SettingsProvider.
Easy bit...Create a class which implements SettingsProvider, use ctrl+. to help you out.
class CustomSettingsProvider : SettingsProvider
Another easy bit...Go to the code behind of your .settings file (there is a button in the designer) and decorate the class to point it to your implementation. This must be done to override the built in functionality, but it does not change how the designer works.(sorry the formatting here is weird)
[System.Configuration.SettingsProvider(typeof(YourCompany.YourProduct.CustomSettingsProvider))]
public sealed partial class Settings
{
//bla bla bla
}
Getting the path: There is a property called "SettingsKey" (e.g. Properties.Settings.Default.SettingsKey) I used this to store the path. I made the following property.
/// <summary>
/// The key this is returning must set before the settings are used.
/// e.g. <c>Properties.Settings.Default.SettingsKey = #"C:\temp\user.config";</c>
/// </summary>
private string UserConfigPath
{
get
{
return Properties.Settings.Default.SettingsKey;
}
}
Storing the settings values. I chose to use a dictionary. This will get used extensively in a bit. I created a struct as a helper.
/// <summary>
/// In memory storage of the settings values
/// </summary>
private Dictionary<string, SettingStruct> SettingsDictionary { get; set; }
/// <summary>
/// Helper struct.
/// </summary>
internal struct SettingStruct
{
internal string name;
internal string serializeAs;
internal string value;
}
The magic. You must override 2 methods, GetPropertyValues and SetPropertyValues. GetPropertyValues recieves as a parameter what you see in the designer, you have to opportunity to update the values and return a new collection. SetPropertyValues is called when the user saves any changes to the values made at runtime, this is where I update the dictionary and write out the file.
/// <summary>
/// Must override this, this is the bit that matches up the designer properties to the dictionary values
/// </summary>
/// <param name="context"></param>
/// <param name="collection"></param>
/// <returns></returns>
public override SettingsPropertyValueCollection GetPropertyValues(SettingsContext context, SettingsPropertyCollection collection)
{
//load the file
if (!_loaded)
{
_loaded = true;
LoadValuesFromFile();
}
//collection that will be returned.
SettingsPropertyValueCollection values = new SettingsPropertyValueCollection();
//iterate thought the properties we get from the designer, checking to see if the setting is in the dictionary
foreach (SettingsProperty setting in collection)
{
SettingsPropertyValue value = new SettingsPropertyValue(setting);
value.IsDirty = false;
//need the type of the value for the strong typing
var t = Type.GetType(setting.PropertyType.FullName);
if (SettingsDictionary.ContainsKey(setting.Name))
{
value.SerializedValue = SettingsDictionary[setting.Name].value;
value.PropertyValue = Convert.ChangeType(SettingsDictionary[setting.Name].value, t);
}
else //use defaults in the case where there are no settings yet
{
value.SerializedValue = setting.DefaultValue;
value.PropertyValue = Convert.ChangeType(setting.DefaultValue, t);
}
values.Add(value);
}
return values;
}
/// <summary>
/// Must override this, this is the bit that does the saving to file. Called when Settings.Save() is called
/// </summary>
/// <param name="context"></param>
/// <param name="collection"></param>
public override void SetPropertyValues(SettingsContext context, SettingsPropertyValueCollection collection)
{
//grab the values from the collection parameter and update the values in our dictionary.
foreach (SettingsPropertyValue value in collection)
{
var setting = new SettingStruct()
{
value = (value.PropertyValue == null ? String.Empty : value.PropertyValue.ToString()),
name = value.Name,
serializeAs = value.Property.SerializeAs.ToString()
};
if (!SettingsDictionary.ContainsKey(value.Name))
{
SettingsDictionary.Add(value.Name, setting);
}
else
{
SettingsDictionary[value.Name] = setting;
}
}
//now that our local dictionary is up-to-date, save it to disk.
SaveValuesToFile();
}
Complete solution. So here's the entire class which includes the constructor, Initialize, and helper methods. Feel free to cut/paste slice and dice.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
using System.Reflection;
using System.Xml.Linq;
using System.IO;
namespace YourCompany.YourProduct
{
class CustomSettingsProvider : SettingsProvider
{
const string NAME = "name";
const string SERIALIZE_AS = "serializeAs";
const string CONFIG = "configuration";
const string USER_SETTINGS = "userSettings";
const string SETTING = "setting";
/// <summary>
/// Loads the file into memory.
/// </summary>
public CustomSettingsProvider()
{
SettingsDictionary = new Dictionary<string, SettingStruct>();
}
/// <summary>
/// Override.
/// </summary>
public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
{
base.Initialize(ApplicationName, config);
}
/// <summary>
/// Override.
/// </summary>
public override string ApplicationName
{
get
{
return System.Reflection.Assembly.GetExecutingAssembly().ManifestModule.Name;
}
set
{
//do nothing
}
}
/// <summary>
/// Must override this, this is the bit that matches up the designer properties to the dictionary values
/// </summary>
/// <param name="context"></param>
/// <param name="collection"></param>
/// <returns></returns>
public override SettingsPropertyValueCollection GetPropertyValues(SettingsContext context, SettingsPropertyCollection collection)
{
//load the file
if (!_loaded)
{
_loaded = true;
LoadValuesFromFile();
}
//collection that will be returned.
SettingsPropertyValueCollection values = new SettingsPropertyValueCollection();
//itterate thought the properties we get from the designer, checking to see if the setting is in the dictionary
foreach (SettingsProperty setting in collection)
{
SettingsPropertyValue value = new SettingsPropertyValue(setting);
value.IsDirty = false;
//need the type of the value for the strong typing
var t = Type.GetType(setting.PropertyType.FullName);
if (SettingsDictionary.ContainsKey(setting.Name))
{
value.SerializedValue = SettingsDictionary[setting.Name].value;
value.PropertyValue = Convert.ChangeType(SettingsDictionary[setting.Name].value, t);
}
else //use defaults in the case where there are no settings yet
{
value.SerializedValue = setting.DefaultValue;
value.PropertyValue = Convert.ChangeType(setting.DefaultValue, t);
}
values.Add(value);
}
return values;
}
/// <summary>
/// Must override this, this is the bit that does the saving to file. Called when Settings.Save() is called
/// </summary>
/// <param name="context"></param>
/// <param name="collection"></param>
public override void SetPropertyValues(SettingsContext context, SettingsPropertyValueCollection collection)
{
//grab the values from the collection parameter and update the values in our dictionary.
foreach (SettingsPropertyValue value in collection)
{
var setting = new SettingStruct()
{
value = (value.PropertyValue == null ? String.Empty : value.PropertyValue.ToString()),
name = value.Name,
serializeAs = value.Property.SerializeAs.ToString()
};
if (!SettingsDictionary.ContainsKey(value.Name))
{
SettingsDictionary.Add(value.Name, setting);
}
else
{
SettingsDictionary[value.Name] = setting;
}
}
//now that our local dictionary is up-to-date, save it to disk.
SaveValuesToFile();
}
/// <summary>
/// Loads the values of the file into memory.
/// </summary>
private void LoadValuesFromFile()
{
if (!File.Exists(UserConfigPath))
{
//if the config file is not where it's supposed to be create a new one.
CreateEmptyConfig();
}
//load the xml
var configXml = XDocument.Load(UserConfigPath);
//get all of the <setting name="..." serializeAs="..."> elements.
var settingElements = configXml.Element(CONFIG).Element(USER_SETTINGS).Element(typeof(Properties.Settings).FullName).Elements(SETTING);
//iterate through, adding them to the dictionary, (checking for nulls, xml no likey nulls)
//using "String" as default serializeAs...just in case, no real good reason.
foreach (var element in settingElements)
{
var newSetting = new SettingStruct()
{
name = element.Attribute(NAME) == null ? String.Empty : element.Attribute(NAME).Value,
serializeAs = element.Attribute(SERIALIZE_AS) == null ? "String" : element.Attribute(SERIALIZE_AS).Value,
value = element.Value ?? String.Empty
};
SettingsDictionary.Add(element.Attribute(NAME).Value, newSetting);
}
}
/// <summary>
/// Creates an empty user.config file...looks like the one MS creates.
/// This could be overkill a simple key/value pairing would probably do.
/// </summary>
private void CreateEmptyConfig()
{
var doc = new XDocument();
var declaration = new XDeclaration("1.0", "utf-8", "true");
var config = new XElement(CONFIG);
var userSettings = new XElement(USER_SETTINGS);
var group = new XElement(typeof(Properties.Settings).FullName);
userSettings.Add(group);
config.Add(userSettings);
doc.Add(config);
doc.Declaration = declaration;
doc.Save(UserConfigPath);
}
/// <summary>
/// Saves the in memory dictionary to the user config file
/// </summary>
private void SaveValuesToFile()
{
//load the current xml from the file.
var import = XDocument.Load(UserConfigPath);
//get the settings group (e.g. <Company.Project.Desktop.Settings>)
var settingsSection = import.Element(CONFIG).Element(USER_SETTINGS).Element(typeof(Properties.Settings).FullName);
//iterate though the dictionary, either updating the value or adding the new setting.
foreach (var entry in SettingsDictionary)
{
var setting = settingsSection.Elements().FirstOrDefault(e => e.Attribute(NAME).Value == entry.Key);
if (setting == null) //this can happen if a new setting is added via the .settings designer.
{
var newSetting = new XElement(SETTING);
newSetting.Add(new XAttribute(NAME, entry.Value.name));
newSetting.Add(new XAttribute(SERIALIZE_AS, entry.Value.serializeAs));
newSetting.Value = (entry.Value.value ?? String.Empty);
settingsSection.Add(newSetting);
}
else //update the value if it exists.
{
setting.Value = (entry.Value.value ?? String.Empty);
}
}
import.Save(UserConfigPath);
}
/// <summary>
/// The setting key this is returning must set before the settings are used.
/// e.g. <c>Properties.Settings.Default.SettingsKey = #"C:\temp\user.config";</c>
/// </summary>
private string UserConfigPath
{
get
{
return Properties.Settings.Default.SettingsKey;
}
}
/// <summary>
/// In memory storage of the settings values
/// </summary>
private Dictionary<string, SettingStruct> SettingsDictionary { get; set; }
/// <summary>
/// Helper struct.
/// </summary>
internal struct SettingStruct
{
internal string name;
internal string serializeAs;
internal string value;
}
bool _loaded;
}
}
You would have to implement your own SettingsProvider to customize the path.
See this Client Settings FAQ
Q: Why is the path so obscure? Is there any way to change/customize it?
A: The path construction algorithm has to meet certain rigorous requirements in terms of security, isolation and robustness. While we tried to make the path as easily discoverable as possible by making use of friendly, application supplied strings, it is not possible to keep the path totally simple without running into issues like collisions with other apps, spoofing etc.
The LocalFileSettingsProvider does not provide a way to change the files in which settings are stored. Note that the provider itself doesn't determine the config file locations in the first place - it is the configuration system. If you need to store the settings in a different location for some reason, the recommended way is to write your own SettingsProvider. This is fairly simple to implement and you can find samples in the .NET 2.0 SDK that show how to do this. Keep in mind however that you may run into the same isolation issues mentioned above .
Here is an easier, briefer alternative to creating a custom settings class: change your app's evidence so that the "url" part is a constant rather than being based on the location of executable. To do this, you need to modify the default AppDomain when the program starts. There are two parts: setting app.config to use your AppDomainManager, and creating an AppDomainManager and HostSecurityManager to customize the Url evidence. Sounds complicated but it's much simpler than creating a custom settings class. This only applies to unsigned assemblies. If you have a signed assembly, it's going to use that evidence instead of the Url. But the good news there is your path will always be constant anyway (as long as the signing key doesn't change).
You can copy the code below and just replace the YourAppName bits.
DefaultAppDomainManager.cs:
using System;
using System.Security;
using System.Security.Policy;
namespace YourAppName
{
/// <summary>
/// A least-evil (?) way of customizing the default location of the application's user.config files.
/// </summary>
public class CustomEvidenceHostSecurityManager : HostSecurityManager
{
public override HostSecurityManagerOptions Flags
{
get
{
return HostSecurityManagerOptions.HostAssemblyEvidence;
}
}
public override Evidence ProvideAssemblyEvidence(System.Reflection.Assembly loadedAssembly, Evidence inputEvidence)
{
if (!loadedAssembly.Location.EndsWith("YourAppName.exe"))
return base.ProvideAssemblyEvidence(loadedAssembly, inputEvidence);
// override the full Url used in Evidence to just "YourAppName.exe" so it remains the same no matter where the exe is located
var zoneEvidence = inputEvidence.GetHostEvidence<Zone>();
return new Evidence(new EvidenceBase[] { zoneEvidence, new Url("YourAppName.exe") }, null);
}
}
public class DefaultAppDomainManager : AppDomainManager
{
private CustomEvidenceHostSecurityManager hostSecurityManager;
public override void InitializeNewDomain(AppDomainSetup appDomainInfo)
{
base.InitializeNewDomain(appDomainInfo);
hostSecurityManager = new CustomEvidenceHostSecurityManager();
}
public override HostSecurityManager HostSecurityManager
{
get
{
return hostSecurityManager;
}
}
}
}
app.config excerpt:
<runtime>
<appDomainManagerType value="YourAppName.DefaultAppDomainManager" />
<appDomainManagerAssembly value="DefaultAppDomainManager, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</runtime>
Building on Chucks excellent answer:
Implement a new partial class based on Settings.Designer.cs, so the Settings Designer does not wipe out the attribute when changes are made:
namespace Worker.Properties
{
[System.Configuration.SettingsProvider(
typeof(SettingsProviders.DllFileSettingsProvider<Settings>))]
internal sealed partial class Settings
{
}
}
I created the following class that can be put in an external project. It allows the config to be project.dll.config. By Inheriting and Overriding ConfigPath allows any path you like. I also added support for reading System.Collections.Specialized.StringCollection. This version is for Application settings.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
using System.Reflection;
using System.Xml.Linq;
using System.IO;
//http://stackoverflow.com/questions/2265271/custom-path-of-the-user-config
namespace SettingsProviders
{
public class DllFileSettingsProvider<Properties_Settings> : SettingsProvider where Properties_Settings : new()
{
const string NAME = "name";
const string SERIALIZE_AS = "serializeAs";
const string CONFIG = "configuration";
const string APPLICATION_SETTINGS = "applicationSettings";
const string SETTING = "setting";
/// <summary>
/// Loads the file into memory.
/// </summary>
public DllFileSettingsProvider()
{
SettingsDictionary = new Dictionary<string, SettingStruct>();
}
/// <summary>
/// Override.
/// </summary>
public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
{
base.Initialize(ApplicationName, config);
}
/// <summary>
/// Override.
/// </summary>
public override string ApplicationName
{
get
{
return System.Reflection.Assembly.GetExecutingAssembly().ManifestModule.Name;
}
set
{
//do nothing
}
}
/// <summary>
/// Must override this, this is the bit that matches up the designer properties to the dictionary values
/// </summary>
/// <param name="context"></param>
/// <param name="collection"></param>
/// <returns></returns>
public override SettingsPropertyValueCollection GetPropertyValues(SettingsContext context, SettingsPropertyCollection collection)
{
//load the file
if (!_loaded)
{
_loaded = true;
LoadValuesFromFile();
}
//collection that will be returned.
SettingsPropertyValueCollection values = new SettingsPropertyValueCollection();
//itterate thought the properties we get from the designer, checking to see if the setting is in the dictionary
foreach (SettingsProperty setting in collection)
{
SettingsPropertyValue value = new SettingsPropertyValue(setting);
value.IsDirty = false;
//need the type of the value for the strong typing
var t = Type.GetType(setting.PropertyType.FullName);
if (setting.PropertyType == typeof(System.Collections.Specialized.StringCollection))
{
var xml = SettingsDictionary[setting.Name].value;
var stringReader = new System.IO.StringReader(xml);
var xmlreader = System.Xml.XmlReader.Create(stringReader);
var ser = new System.Xml.Serialization.XmlSerializer(typeof(System.Collections.Specialized.StringCollection));
var obj = ser.Deserialize(xmlreader);
var col = (System.Collections.Specialized.StringCollection)obj;
value.PropertyValue = col;
}
else if (SettingsDictionary.ContainsKey(setting.Name))
{
value.SerializedValue = SettingsDictionary[setting.Name].value;
value.PropertyValue = Convert.ChangeType(SettingsDictionary[setting.Name].value, t);
}
else //use defaults in the case where there are no settings yet
{
value.SerializedValue = setting.DefaultValue;
value.PropertyValue = Convert.ChangeType(setting.DefaultValue, t);
}
values.Add(value);
}
return values;
}
/// <summary>
/// Must override this, this is the bit that does the saving to file. Called when Settings.Save() is called
/// </summary>
/// <param name="context"></param>
/// <param name="collection"></param>
public override void SetPropertyValues(SettingsContext context, SettingsPropertyValueCollection collection)
{
//grab the values from the collection parameter and update the values in our dictionary.
foreach (SettingsPropertyValue value in collection)
{
var setting = new SettingStruct()
{
value = (value.PropertyValue == null ? String.Empty : value.PropertyValue.ToString()),
name = value.Name,
serializeAs = value.Property.SerializeAs.ToString()
};
if (!SettingsDictionary.ContainsKey(value.Name))
{
SettingsDictionary.Add(value.Name, setting);
}
else
{
SettingsDictionary[value.Name] = setting;
}
}
//now that our local dictionary is up-to-date, save it to disk.
SaveValuesToFile();
}
/// <summary>
/// Loads the values of the file into memory.
/// </summary>
private void LoadValuesFromFile()
{
if (!File.Exists(ConfigPath))
{
//if the config file is not where it's supposed to be create a new one.
throw new Exception("Config file not found: " + ConfigPath);
}
//load the xml
var configXml = XDocument.Load(ConfigPath);
//get all of the <setting name="..." serializeAs="..."> elements.
var settingElements = configXml.Element(CONFIG).Element(APPLICATION_SETTINGS).Element(typeof(Properties_Settings).FullName).Elements(SETTING);
//iterate through, adding them to the dictionary, (checking for nulls, xml no likey nulls)
//using "String" as default serializeAs...just in case, no real good reason.
foreach (var element in settingElements)
{
var newSetting = new SettingStruct()
{
name = element.Attribute(NAME) == null ? String.Empty : element.Attribute(NAME).Value,
serializeAs = element.Attribute(SERIALIZE_AS) == null ? "String" : element.Attribute(SERIALIZE_AS).Value ,
value = element.Value ?? String.Empty
};
if (newSetting.serializeAs == "Xml")
{
var e = (XElement)element.Nodes().First();
newSetting.value = e.LastNode.ToString() ?? String.Empty;
};
SettingsDictionary.Add(element.Attribute(NAME).Value, newSetting);
}
}
/// <summary>
/// Creates an empty user.config file...looks like the one MS creates.
/// This could be overkill a simple key/value pairing would probably do.
/// </summary>
private void CreateEmptyConfig()
{
var doc = new XDocument();
var declaration = new XDeclaration("1.0", "utf-8", "true");
var config = new XElement(CONFIG);
var userSettings = new XElement(APPLICATION_SETTINGS);
var group = new XElement(typeof(Properties_Settings).FullName);
userSettings.Add(group);
config.Add(userSettings);
doc.Add(config);
doc.Declaration = declaration;
doc.Save(ConfigPath);
}
/// <summary>
/// Saves the in memory dictionary to the user config file
/// </summary>
private void SaveValuesToFile()
{
//load the current xml from the file.
var import = XDocument.Load(ConfigPath);
//get the settings group (e.g. <Company.Project.Desktop.Settings>)
var settingsSection = import.Element(CONFIG).Element(APPLICATION_SETTINGS).Element(typeof(Properties_Settings).FullName);
//iterate though the dictionary, either updating the value or adding the new setting.
foreach (var entry in SettingsDictionary)
{
var setting = settingsSection.Elements().FirstOrDefault(e => e.Attribute(NAME).Value == entry.Key);
if (setting == null) //this can happen if a new setting is added via the .settings designer.
{
var newSetting = new XElement(SETTING);
newSetting.Add(new XAttribute(NAME, entry.Value.name));
newSetting.Add(new XAttribute(SERIALIZE_AS, entry.Value.serializeAs));
newSetting.Value = (entry.Value.value ?? String.Empty);
settingsSection.Add(newSetting);
}
else //update the value if it exists.
{
setting.Value = (entry.Value.value ?? String.Empty);
}
}
import.Save(ConfigPath);
}
/// <summary>
/// The setting key this is returning must set before the settings are used.
/// e.g. <c>Properties.Settings.Default.SettingsKey = #"C:\temp\user.config";</c>
/// </summary>
public virtual string ConfigPath
{
get
{
var name = new Properties_Settings().GetType().Module.Name + ".config";
if (System.IO.File.Exists(name)==false)
{
System.Diagnostics.Trace.WriteLine("config file NOT found:" + name);
}
System.Diagnostics.Trace.WriteLine("config file found:" + name);
return name;
// return Properties.Settings.Default.SettingsKey;
}
}
/// <summary>
/// In memory storage of the settings values
/// </summary>
internal Dictionary<string, SettingStruct> SettingsDictionary { get; set; }
/// <summary>
/// Helper struct.
/// </summary>
internal struct SettingStruct
{
internal string name;
internal string serializeAs;
internal string value;
}
bool _loaded;
}
}
Make sure dependent projects include the project.dll.config file by add a post-build event:
copy $(SolutionDir)Worker\$(OutDir)Worker.dll.config $(TargetDir) /y
Related
I used https://learn.microsoft.com/en-us/aspnet/core/tutorials/getting-started-with-swashbuckle?view=aspnetcore-2.1&tabs=visual-studio#xml-comments to show my classes summaries description in SwaggerUI, it's OK but not show enum summary description !
My startup.cs
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info
{
Version = "v1",
Title = "My App-Service",
Description = "My Description",
});
c.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"));
c.DescribeAllEnumsAsStrings();
});
My enum:
public enum GenderEnum
{
/// <summary>
/// Man Description
/// </summary>
Man = 1,
/// <summary>
/// Woman Description
/// </summary>
Woman = 2
}
It shows something like following:
I want to show Man Description and Woman Description in SwaggerUI
like this:
Man = 1, Man Description
Woman = 2, Woman Description
I'm using Swashbuckle.AspNetCore v4.0.1 package
As of June/2021 OpenApi now supports this and you can extend Swagger to show the details. Here is my code for C# on .NET 5.0.
First define the schema filter in a file (call it DescribeEnumMembers.cs and be sure to change YourNamespace to the name of your namespace):
using System;
using System.Text;
using System.Xml.Linq;
using System.Xml.XPath;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace YourNamespace
{
/// <summary>
/// Swagger schema filter to modify description of enum types so they
/// show the XML docs attached to each member of the enum.
/// </summary>
public class DescribeEnumMembers: ISchemaFilter
{
private readonly XDocument mXmlComments;
/// <summary>
/// Initialize schema filter.
/// </summary>
/// <param name="argXmlComments">Document containing XML docs for enum members.</param>
public DescribeEnumMembers(XDocument argXmlComments)
=> mXmlComments = argXmlComments;
/// <summary>
/// Apply this schema filter.
/// </summary>
/// <param name="argSchema">Target schema object.</param>
/// <param name="argContext">Schema filter context.</param>
public void Apply(OpenApiSchema argSchema, SchemaFilterContext argContext) {
var EnumType = argContext.Type;
if(!EnumType.IsEnum) return;
var sb = new StringBuilder(argSchema.Description);
sb.AppendLine("<p>Possible values:</p>");
sb.AppendLine("<ul>");
foreach(var EnumMemberName in Enum.GetNames(EnumType)) {
var FullEnumMemberName = $"F:{EnumType.FullName}.{EnumMemberName}";
var EnumMemberDescription = mXmlComments.XPathEvaluate(
$"normalize-space(//member[#name = '{FullEnumMemberName}']/summary/text())"
) as string;
if(string.IsNullOrEmpty(EnumMemberDescription)) continue;
sb.AppendLine($"<li><b>{EnumMemberName}</b>: {EnumMemberDescription}</li>");
}
sb.AppendLine("</ul>");
argSchema.Description = sb.ToString();
}
}
}
Then enable it in your ASP.NET ConfigureServices() method. Here is my code after snipping out the parts that don't matter for this exercise:
public void ConfigureServices(IServiceCollection argServices) {
// ...<snip other code>
argServices.AddSwaggerGen(SetSwaggerGenOptions);
// ...<snip other code>
return;
// ...<snip other code>
void SetSwaggerGenOptions(SwaggerGenOptions argOptions) {
// ...<snip other code>
AddXmlDocs();
return;
void AddXmlDocs() {
// generate paths for the XML doc files in the assembly's directory.
var XmlDocPaths = Directory.GetFiles(
path: AppDomain.CurrentDomain.BaseDirectory,
searchPattern: "YourAssemblyNameHere*.xml"
);
// load the XML docs for processing.
var XmlDocs = (
from DocPath in XmlDocPaths select XDocument.Load(DocPath)
).ToList();
// ...<snip other code>
// add pre-processed XML docs to Swagger.
foreach(var doc in XmlDocs) {
argOptions.IncludeXmlComments(() => new XPathDocument(doc.CreateReader()), true);
// apply schema filter to add description of enum members.
argOptions.SchemaFilter<DescribeEnumMembers>(doc);
}
}
}
}
Remember to change "YourAssemblyNameHere*.xml" to match your assembly name. The important line that enables the schema filter is:
argOptions.SchemaFilter<DescribeEnumMembers>(doc);
...which MUST be called AFTER the following line:
argOptions.IncludeXmlComments(() => new XPathDocument(doc.CreateReader()), true);
Using the above code, if you have an enum type defined like this for example:
/// <summary>
/// Setting to control when a no-match report is returned when searching.
/// </summary>
public enum NoMatchReportSetting
{
/// <summary>
/// Return no-match report only if the search query has no match.
/// </summary>
IfNoMatch = 0,
/// <summary>
/// Always return no-match report even if the search query has a match.
/// </summary>
Always = 1,
/// <summary>
/// Never return no-match report even if search query has no match.
/// </summary>
No = 99
}
The Swagger documentation will end up showing a description of each enum member as part of the description of the enum type itself:
This solution allows for
Show underlying value as well as name/description
Handle multiple xml documentation files, but only process docs once.
Customization of the layout without code change
Here's the class...
using System;
using System.Collections;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using System.Xml.XPath;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace YourNamespace
{
/// <summary>
/// Swagger schema filter to modify description of enum types so they
/// show the XML docs attached to each member of the enum.
/// </summary>
public class DescribeEnumMembers : ISchemaFilter
{
private readonly XDocument xmlComments;
private readonly string assemblyName;
/// <summary>
/// Initialize schema filter.
/// </summary>
/// <param name="xmlComments">Document containing XML docs for enum members.</param>
public DescribeEnumMembers(XDocument xmlComments)
{
this.xmlComments = xmlComments;
this.assemblyName = DetermineAssembly(xmlComments);
}
/// <summary>
/// Pre-amble to use before the enum items
/// </summary>
public static string Prefix { get; set; } = "<p>Possible values:</p>";
/// <summary>
/// Format to use, 0 : value, 1: Name, 2: Description
/// </summary>
public static string Format { get; set; } = "<b>{0} - {1}</b>: {2}";
/// <summary>
/// Apply this schema filter.
/// </summary>
/// <param name="schema">Target schema object.</param>
/// <param name="context">Schema filter context.</param>
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
var type = context.Type;
// Only process enums and...
if (!type.IsEnum)
{
return;
}
// ...only the comments defined in their origin assembly
if (type.Assembly.GetName().Name != assemblyName)
{
return;
}
var sb = new StringBuilder(schema.Description);
if (!string.IsNullOrEmpty(Prefix))
{
sb.AppendLine(Prefix);
}
sb.AppendLine("<ul>");
// TODO: Handle flags better e.g. Hex formatting
foreach (var name in Enum.GetValues(type))
{
// Allows for large enums
var value = Convert.ToInt64(name);
var fullName = $"F:{type.FullName}.{name}";
var description = xmlComments.XPathEvaluate(
$"normalize-space(//member[#name = '{fullName}']/summary/text())"
) as string;
sb.AppendLine(string.Format("<li>" + Format + "</li>", value, name, description));
}
sb.AppendLine("</ul>");
schema.Description = sb.ToString();
}
private string DetermineAssembly(XDocument doc)
{
var name = ((IEnumerable)doc.XPathEvaluate("/doc/assembly")).Cast<XElement>().ToList().FirstOrDefault();
return name?.Value;
}
}
}
and utilization...
services.AddSwaggerGen(c =>
{
...
// See https://github.com/domaindrivendev/Swashbuckle/issues/86
var dir = new DirectoryInfo(AppContext.BaseDirectory);
foreach (var fi in dir.EnumerateFiles("*.xml"))
{
var doc = XDocument.Load(fi.FullName);
c.IncludeXmlComments(() => new XPathDocument(doc.CreateReader()), true);
c.SchemaFilter<DescribeEnumMembers>(doc);
}
});
This then reports as
Unfortunately this does not seem to be supported in the OpenAPI specification. There is an open Github issue for this.
There is also an open Github issue for Swashbuckle.
However, in this issue there is a workaround to create a schema filter, which at least shows the enum value comments in the enum type description.
I solved this using a description attribute. Here is an example usage:
public enum GenderEnum
{
[Description("Man Description")]
Man = 1,
[Description("Woman Description")]
Woman = 2
}
In order to get a sorted aggregated string, I wrote the CLR function below. However, it always returns empty instead of what I expected, just like "001, 002, 003". I tried to debug the CLR function in visual studio 2017, but threw the error message
The operation could not be completed. Unspecified error
Code:
[Serializable]
[SqlUserDefinedAggregate(
Format.UserDefined, //use clr serialization to serialize the intermediate result
Name = "CLRSortedCssvAgg", //aggregate name on sql
IsInvariantToNulls = true, //optimizer property
IsInvariantToDuplicates = false, //optimizer property
IsInvariantToOrder = false, //optimizer property
IsNullIfEmpty = false, //optimizer property
MaxByteSize = -1) //maximum size in bytes of persisted value
]
public class SortedCssvConcatenateAgg : IBinarySerialize
{
/// <summary>
/// The variable that holds all the strings to be aggregated.
/// </summary>
List<string> aggregationList;
StringBuilder accumulator;
/// <summary>
/// Separator between concatenated values.
/// </summary>
const string CommaSpaceSeparator = ", ";
/// <summary>
/// Initialize the internal data structures.
/// </summary>
public void Init()
{
accumulator = new StringBuilder();
aggregationList = new List<string>();
}
/// <summary>
/// Accumulate the next value, not if the value is null or empty.
/// </summary>
public void Accumulate(SqlString value)
{
if (value.IsNull || String.IsNullOrEmpty(value.Value))
{
return;
}
aggregationList.Add(value.Value);
}
/// <summary>
/// Merge the partially computed aggregate with this aggregate.
/// </summary>
/// <param name="other"></param>
public void Merge(SortedCssvConcatenateAgg other)
{
aggregationList.AddRange(other.aggregationList);
}
/// <summary>
/// Called at the end of aggregation, to return the results of the aggregation.
/// </summary>
/// <returns></returns>
public SqlString Terminate()
{
if (aggregationList != null && aggregationList.Count > 0)
{
aggregationList.Sort();
accumulator.Append(string.Join(CommaSpaceSeparator, aggregationList));
aggregationList.Clear();
}
return new SqlString(accumulator.ToString());
}
public void Read(BinaryReader r)
{
accumulator = new StringBuilder(r.ReadString());
}
public void Write(BinaryWriter w)
{
w.Write(accumulator.ToString());
}
}
You are close. Just need a few minor adjustments. Do the following and it will work (I tested it):
Remove all references to accumulator. It is not used.
Replace the Terminate(), Read(), and Write() methods with the following:
public SqlString Terminate()
{
string _Aggregation = null;
if (aggregationList != null && aggregationList.Count > 0)
{
aggregationList.Sort();
_Aggregation = string.Join(CommaSpaceSeparator, aggregationList);
}
return new SqlString(_Aggregation);
}
public void Read(BinaryReader r)
{
int _Count = r.ReadInt32();
aggregationList = new List<string>(_Count);
for (int _Index = 0; _Index < _Count; _Index++)
{
aggregationList.Add(r.ReadString());
}
}
public void Write(BinaryWriter w)
{
w.Write(aggregationList.Count);
foreach (string _Item in aggregationList)
{
w.Write(_Item);
}
}
That said, I'm not sure if this approach is faster or slower than the FOR XML approach, but a UDA certainly makes for a more readable query, especially if you need multiple aggregations.
Still, I should mention that starting in SQL Server 2017, this became a built-in function: STRING_AGG (which allows for sorting via the WITHIN GROUP (ORDER BY ... ) clause).
In your Accumulate and Merge, you're dealing with your aggregationList; in Read and Write you're dealing with accumulator. You should pick one or the other for all of them and use it. As I understand it, Read and Write are used when the engine needs to persist temporary results to a work table. For your case, when it does that, it's persisting only your empty StringBuilder.
I have an asp.net mvc 5 app with a database that stores photos. I'm trying to read the photo and resize it for display in a Staff profile.
I'm fairly new to both asp.net mvc and c#
I setup the following controller but am not getting an image display when I use a link to the controller in an img tag.
Any help would be appreciated.
public ActionResult Index(int id)
{
Staff staff = db.StaffList.Find(id);
if (staff.Photo != null)
{
var img = new WebImage(staff.Photo);
img.Resize(100, 100, true, true);
var imgBytes = img.GetBytes();
return File(imgBytes, "image/" + img.ImageFormat);
}
else
{
return null;
}
}
Looking around it seems there's a lot of dissatisfaction with the WebImage class and it has a few prominent bugs. I settled on using a nuget package called ImageProcessor rather than trying to write my own. This seems fairly inefficient to me but I don't have a better answer right now and this isn't heavily used so I'm going with this and just moving on.
Posting it here in case anyone else is struggling with something similar.
public ActionResult Index(int id, int? height, int? width)
{
int h = (height ?? 325);
int w = (width ?? 325);
Staff staff = db.StaffList.Find(id);
if (staff == null)
{
return new HttpNotFoundResult();
}
if (staff.Photo != null)
{
Size size = new Size(w, h);
byte[] rawImg;
using (MemoryStream inStream = new MemoryStream(staff.Photo))
{
using (MemoryStream outStream = new MemoryStream())
{
using (ImageFactory imageFactory = new ImageFactory())
{
imageFactory.Load(inStream)
.Constrain(size)
.Format(format)
.Save(outStream);
}
rawImg = outStream.ToArray();
}
}
return new FileContentResult(rawImg, "image/jpeg");
}
else
{
return null;
}
}
I'm going to answer this using ImageProcessor since your own answer uses the library. Disclaimer. I'm also the author of the library.
You're really not best using an ActionResult for processing images as it will be horribly inefficient. It run's far too late in the pipeline and you will have no caching.
You're much better off installing the Imageprocessor.Web package and implementing your own version of the IImageService interface to serve your images from the database. (On that note storing images in a db unless you are using blob storage is never wise either)
Implementing the IImageService interface allows you to utilise the url api within the library with a specific prefix to identify which image service to use. For example, remote image requests are prefixed with remote.axd to tell IageProcessor.Web to excute the RemoteImageService implementation.
This has the benefit of caching your images so subsequent requests are returned from a cache rather than being reprocessed upon each request.
Here is the full implementation of the LocalFileImageService which is the default service for the library. This should be able to serve as a guide for how to implement your own service.
namespace ImageProcessor.Web.Services
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Web;
using ImageProcessor.Web.Helpers;
/// <summary>
/// The local file image service for retrieving images from the
/// file system.
/// </summary>
public class LocalFileImageService : IImageService
{
/// <summary>
/// The prefix for the given implementation.
/// </summary>
private string prefix = string.Empty;
/// <summary>
/// Gets or sets the prefix for the given implementation.
/// <remarks>
/// This value is used as a prefix for any image requests
/// that should use this service.
/// </remarks>
/// </summary>
public string Prefix
{
get
{
return this.prefix;
}
set
{
this.prefix = value;
}
}
/// <summary>
/// Gets a value indicating whether the image service
/// requests files from
/// the locally based file system.
/// </summary>
public bool IsFileLocalService
{
get
{
return true;
}
}
/// <summary>
/// Gets or sets any additional settings required by the service.
/// </summary>
public Dictionary<string, string> Settings { get; set; }
/// <summary>
/// Gets or sets the white list of <see cref="System.Uri"/>.
/// </summary>
public Uri[] WhiteList { get; set; }
/// <summary>
/// Gets a value indicating whether the current request
/// passes sanitizing rules.
/// </summary>
/// <param name="path">
/// The image path.
/// </param>
/// <returns>
/// <c>True</c> if the request is valid; otherwise, <c>False</c>.
/// </returns>
public bool IsValidRequest(string path)
{
return ImageHelpers.IsValidImageExtension(path);
}
/// <summary>
/// Gets the image using the given identifier.
/// </summary>
/// <param name="id">
/// The value identifying the image to fetch.
/// </param>
/// <returns>
/// The <see cref="System.Byte"/> array containing the image data.
/// </returns>
public async Task<byte[]> GetImage(object id)
{
string path = id.ToString();
byte[] buffer;
// Check to see if the file exists.
// ReSharper disable once AssignNullToNotNullAttribute
FileInfo fileInfo = new FileInfo(path);
if (!fileInfo.Exists)
{
throw new HttpException(404, "No image exists at " + path);
}
using (FileStream file = new FileStream(path,
FileMode.Open,
FileAccess.Read,
FileShare.Read,
4096,
true))
{
buffer = new byte[file.Length];
await file.ReadAsync(buffer, 0, (int)file.Length);
}
return buffer;
}
}
}
I have a string array of some file paths:
path/to/folder/file.xxx
path/to/other/
path/to/file/file.xx
path/file.x
path/
How can I convert this list to a tree structure? So far I have the following:
/// <summary>
/// Enumerates types of filesystem nodes.
/// </summary>
public enum FilesystemNodeType
{
/// <summary>
/// Indicates that the node is a file.
/// </summary>
File,
/// <summary>
/// Indicates that the node is a folder.
/// </summary>
Folder
}
/// <summary>
/// Represents a file or folder node.
/// </summary>
public class FilesystemNode
{
private readonly ICollection<FilesystemNode> _children;
/// <summary>
/// Initializes a new instance of the <see cref="FilesystemNode"/> class.
/// </summary>
public FilesystemNode()
{
_children = new LinkedList<FilesystemNode>();
}
/// <summary>
/// Gets or sets the name of the file or folder.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Gets or sets the full path to the file or folder from the root.
/// </summary>
public string Path { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the node is a file or folder.
/// </summary>
public FilesystemNodeType Type { get; set; }
/// <summary>
/// Gets a list of child nodes of this node. The node type must be a folder to have children.
/// </summary>
public ICollection<FilesystemNode> Children
{
get
{
if (Type == FilesystemNodeType.Folder)
return _children;
throw new InvalidOperationException("File nodes cannot have children");
}
}
}
I'm just a bit at a loss at how to actually split up the paths and all. Any path that ends with a / is a directory, any one that doesn't, is not.
Also, while my input will always contain a path to the folder, how would I account for that situation if it did not?
For example, if I had the input:
path/to/file.c
path/file.c
path/
How would I account for the fact that path/to/ is not in the input?
Here is a solution that generates a nested dictionary of NodeEntry items (you can substitute your file info class as needed):
public class NodeEntry
{
public NodeEntry()
{
this.Children = new NodeEntryCollection();
}
public string Key { get; set; }
public NodeEntryCollection Children { get; set; }
}
public class NodeEntryCollection : Dictionary<string, NodeEntry>
{
public void AddEntry(string sEntry, int wBegIndex)
{
if (wBegIndex < sEntry.Length)
{
string sKey;
int wEndIndex;
wEndIndex = sEntry.IndexOf("/", wBegIndex);
if (wEndIndex == -1)
{
wEndIndex = sEntry.Length;
}
sKey = sEntry.Substring(wBegIndex, wEndIndex - wBegIndex);
if (!string.IsNullOrEmpty(sKey)) {
NodeEntry oItem;
if (this.ContainsKey(sKey)) {
oItem = this[sKey];
} else {
oItem = new NodeEntry();
oItem.Key = sKey;
this.Add(sKey, oItem);
}
// Now add the rest to the new item's children
oItem.Children.AddEntry(sEntry, wEndIndex + 1);
}
}
}
}
To use the above, create a new collection:
NodeEntryCollection cItems = new NodeEntryCollection();
then, for each line in your list:
cItems.AddEntry(sLine, 0);
I have been inspired from competent_tech's answer and replaced the Dictionary<string, NodeEntry> with a "simple" ObservableCollection<NodeEntry> as the "Key" information would be stored twice in this Dictionary: once as key of the Dictionary and once as public property in the NodeEntry class.
So my sample based on the "competent_tech" code looks like the following:
public class NodeEntryObservableCollection : ObservableCollection<NodeEntry>
{
public const string DefaultSeparator = "/";
public NodeEntryObservableCollection(string separator = DefaultSeparator)
{
Separator = separator; // default separator
}
/// <summary>
/// Gets or sets the separator used to split the hierarchy.
/// </summary>
/// <value>
/// The separator.
/// </value>
public string Separator { get; set; }
public void AddEntry(string entry)
{
AddEntry(entry, 0);
}
/// <summary>
/// Parses and adds the entry to the hierarchy, creating any parent entries as required.
/// </summary>
/// <param name="entry">The entry.</param>
/// <param name="startIndex">The start index.</param>
public void AddEntry(string entry, int startIndex)
{
if (startIndex >= entry.Length)
{
return;
}
var endIndex = entry.IndexOf(Separator, startIndex);
if (endIndex == -1)
{
endIndex = entry.Length;
}
var key = entry.Substring(startIndex, endIndex - startIndex);
if (string.IsNullOrEmpty(key))
{
return;
}
NodeEntry item;
item = this.FirstOrDefault(n => n.Key == key);
if (item == null)
{
item = new NodeEntry(Separator) { Key = key };
Add(item);
}
// Now add the rest to the new item's children
item.Children.AddEntry(entry, endIndex + 1);
}
}
public class NodeEntry
{
public string Key { get; set; }
public NodeEntryObservableCollection Children { get; set; }
public NodeEntry(string separator = NodeEntryObservableCollection.DefaultSeparator)
{
Children = new NodeEntryObservableCollection(separator);
}
}
This helps me in binding the data in a TreeView like this:
<TreeView Name="trvMyTreeView">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:NodeEntry}" ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Key}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
With a sample code behind like this:
IList<string> pathes = new List<string>
{
"localhost",
"remotehost.levelD.levelDB",
"localhost.level1.level11",
"localhost.level1",
"remotehost.levelD.levelDA",
"localhost.level2.level22",
"remotehost.levelA",
"remotehost",
"remotehost.levelB",
"remotehost.levelD",
"localhost.level2",
"remotehost.levelC"
};
SortedSet<string> sortedPathes = new SortedSet<string>(pathes);
var obsCollection = new NodeEntryObservableCollection(".");
foreach (var p in sortedPathes) { obsCollection.AddEntry(p); }
trvMyTreeView.ItemsSource = obsCollection;
Split each line by the '/' character. If the string array is of length 5, then the first four items should be directories, and you have to test the last for an extension:
string.IsNullOrEmpty(new FileInfo("test").Extension)
If, like in your case, there is always a '/' even for the last directory, then the last item of the split string array is empty.
The rest is just about traversing your tree. When parsing a item, check if the first directory exists in the Children property of your root node. If it not exists, add it, if it does, use this one and go further.
I'm looking to find out a way of seeing when a file was last modified in C#. I have full access to the file.
System.IO.File.GetLastWriteTime is what you need.
You simply want the File.GetLastWriteTime static method.
Example:
var lastModified = System.IO.File.GetLastWriteTime("C:\foo.bar");
Console.WriteLine(lastModified.ToString("dd/MM/yy HH:mm:ss"));
Note however that in the rare case the last-modified time is not updated by the system when writing to the file (this can happen intentionally as an optimisation for high-frequency writing, e.g. logging, or as a bug), then this approach will fail, and you will instead need to subscribe to file write notifications from the system, constantly listening.
Be aware that the function File.GetLastWriteTime does not always work as expected, the values are sometimes not instantaneously updated by the OS. You may get an old Timestamp, even if the file has been modified right before.
The behaviour may vary between OS versions. For example, this unit test worked well every time on my developer machine, but it always fails on our build server.
[TestMethod]
public void TestLastModifiedTimeStamps()
{
var tempFile = Path.GetTempFileName();
var lastModified = File.GetLastWriteTime(tempFile);
using (new FileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.None))
{
}
Assert.AreNotEqual(lastModified, File.GetLastWriteTime(tempFile));
}
See File.GetLastWriteTime seems to be returning 'out of date' value
Your options:
a) live with the occasional omissions.
b) Build up an active component realising the observer pattern (eg. a tcp server client structure), communicating the changes directly instead of writing / reading files. Fast and flexible, but another dependency and a possible point of failure (and some work, of course).
c) Ensure the signalling process by replacing the content of a dedicated signal file that other processes regularly read. It´s not that smart as it´s a polling procedure and has a greater overhead than calling File.GetLastWriteTime, but if not checking the content from too many places too often, it will do the work.
/// <summary>
/// type to set signals or check for them using a central file
/// </summary>
public class FileSignal
{
/// <summary>
/// path to the central file for signal control
/// </summary>
public string FilePath { get; private set; }
/// <summary>
/// numbers of retries when not able to retrieve (exclusive) file access
/// </summary>
public int MaxCollisions { get; private set; }
/// <summary>
/// timespan to wait until next try
/// </summary>
public TimeSpan SleepOnCollisionInterval { get; private set; }
/// <summary>
/// Timestamp of the last signal
/// </summary>
public DateTime LastSignal { get; private set; }
/// <summary>
/// constructor
/// </summary>
/// <param name="filePath">path to the central file for signal control</param>
/// <param name="maxCollisions">numbers of retries when not able to retrieve (exclusive) file access</param>
/// <param name="sleepOnCollisionInterval">timespan to wait until next try </param>
public FileSignal(string filePath, int maxCollisions, TimeSpan sleepOnCollisionInterval)
{
FilePath = filePath;
MaxCollisions = maxCollisions;
SleepOnCollisionInterval = sleepOnCollisionInterval;
LastSignal = GetSignalTimeStamp();
}
/// <summary>
/// constructor using a default value of 50 ms for sleepOnCollisionInterval
/// </summary>
/// <param name="filePath">path to the central file for signal control</param>
/// <param name="maxCollisions">numbers of retries when not able to retrieve (exclusive) file access</param>
public FileSignal(string filePath, int maxCollisions): this (filePath, maxCollisions, TimeSpan.FromMilliseconds(50))
{
}
/// <summary>
/// constructor using a default value of 50 ms for sleepOnCollisionInterval and a default value of 10 for maxCollisions
/// </summary>
/// <param name="filePath">path to the central file for signal control</param>
public FileSignal(string filePath) : this(filePath, 10)
{
}
private Stream GetFileStream(FileAccess fileAccess)
{
var i = 0;
while (true)
{
try
{
return new FileStream(FilePath, FileMode.Create, fileAccess, FileShare.None);
}
catch (Exception e)
{
i++;
if (i >= MaxCollisions)
{
throw e;
}
Thread.Sleep(SleepOnCollisionInterval);
};
};
}
private DateTime GetSignalTimeStamp()
{
if (!File.Exists(FilePath))
{
return DateTime.MinValue;
}
using (var stream = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.None))
{
if(stream.Length == 0)
{
return DateTime.MinValue;
}
using (var reader = new BinaryReader(stream))
{
return DateTime.FromBinary(reader.ReadInt64());
};
}
}
/// <summary>
/// overwrites the existing central file and writes the current time into it.
/// </summary>
public void Signal()
{
LastSignal = DateTime.Now;
using (var stream = new FileStream(FilePath, FileMode.Create, FileAccess.Write, FileShare.None))
{
using (var writer = new BinaryWriter(stream))
{
writer.Write(LastSignal.ToBinary());
}
}
}
/// <summary>
/// returns true if the file signal has changed, otherwise false.
/// </summary>
public bool CheckIfSignalled()
{
var signal = GetSignalTimeStamp();
var signalTimestampChanged = LastSignal != signal;
LastSignal = signal;
return signalTimestampChanged;
}
}
Some tests for it:
[TestMethod]
public void TestSignal()
{
var fileSignal = new FileSignal(Path.GetTempFileName());
var fileSignal2 = new FileSignal(fileSignal.FilePath);
Assert.IsFalse(fileSignal.CheckIfSignalled());
Assert.IsFalse(fileSignal2.CheckIfSignalled());
Assert.AreEqual(fileSignal.LastSignal, fileSignal2.LastSignal);
fileSignal.Signal();
Assert.IsFalse(fileSignal.CheckIfSignalled());
Assert.AreNotEqual(fileSignal.LastSignal, fileSignal2.LastSignal);
Assert.IsTrue(fileSignal2.CheckIfSignalled());
Assert.AreEqual(fileSignal.LastSignal, fileSignal2.LastSignal);
Assert.IsFalse(fileSignal2.CheckIfSignalled());
}
Just use File.GetLastWriteTime. There's a sample on that page showing how to use it.
The actual real last modified DateTime is in Microsoft.VisualBasic.FileSystem.FileDateTime(pathString).
LastWriteTime will give you a false positive if the file has been copied from its original location.
See https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualbasic.filesystem.filedatetime?view=netframework-4.8
Also: I have noticed that some files transferred between computers can differ in their modified DateTime by a second or so. NFI why. I added a 5-second fudge-factor to my check to account for this discrepancy.