I'm trying to write a command line tool that modifies some code using Roslyn. Everything seems to go well: the solution is opened, the solution is changed, the Workspace.TryApplyChanges method returns true. However no actual files are changed on disk. What's up? Below is the top level code I'm using.
static void Main(string[] args)
{
var solutionPath = args[0];
UpdateAnnotations(solutionPath).Wait();
}
static async Task<bool> UpdateAnnotations(string solutionPath)
{
using (var workspace = MSBuildWorkspace.Create())
{
var solution = await workspace.OpenSolutionAsync(solutionPath);
var newSolution = await SolutionAttributeUpdater.UpdateAttributes(solution);
var result = workspace.TryApplyChanges(newSolution);
Console.WriteLine(result);
return result;
}
}
I constructed a short program using your code and received the results I expected - the problem appears to reside within the SolutionAttributeUpdater.UpdateAttributes method. I received these results using the following implementation with your base main and UpdateAnnotations-methods:
public class SolutionAttributeUpdater
{
public static async Task<Solution> UpdateAttributes(Solution solution)
{
foreach (var project in solution.Projects)
{
foreach (var document in project.Documents)
{
var syntaxTree = await document.GetSyntaxTreeAsync();
var root = syntaxTree.GetRoot();
var descentants = root.DescendantNodes().Where(curr => curr is AttributeListSyntax).ToList();
if (descentants.Any())
{
var attributeList = SyntaxFactory.AttributeList(
SyntaxFactory.SingletonSeparatedList(
SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("Cookies"), SyntaxFactory.AttributeArgumentList(SyntaxFactory.SeparatedList(new[] { SyntaxFactory.AttributeArgument(
SyntaxFactory.LiteralExpression(
SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(#"Sample"))
)})))));
root = root.ReplaceNodes(descentants, (node, n2) => attributeList);
solution = solution.WithDocumentSyntaxRoot(document.Id, root);
}
}
}
return solution;
}
}
It was tested using the following class in the sample solution:
public class SampleClass<T>
{
[DataMember("Id")]
public int Property { get; set; }
[DataMember("Id")]
public void DoStuff()
{
DoStuff();
}
}
And it resulted in the following Output:
public class SampleClass<T>
{
[Cookies("Sample")] public int Property { get; set; }
[Cookies("Sample")] public void DoStuff()
{
DoStuff();
}
}
If you take a look at the UpdateAttributes method I had to replace the nodes with ReplaceNodes and updated the solution by calling WithDocumentSyntaxRoot.
I would assume that either one of those two calls is missing or that nothing was changed at all - if you call workspace.TryApplyChanges(solution) you would still receive true as an Output.
Note that using multiple calls of root.ReplaceNode() instead of root.ReplaceNodes() can also result in an error since only the first update is actually used for the modified document - which might lead you to believe that nothing has changed at all, depending on the implementation.
I am trying to map out the dependency matrix for a collection of assemblies including what dependency methods are used where. The basic DLL dependency matrix was easy but I am finding it difficult to get the method mapping. The tool I have been using is jbevain MethodBaseRocks.cs.
The dependencies of the assembly I want to parse are being loaded according to AppDomain.CurrentDomain.GetAssemblies() but I am getting FileNotFoundException and ReflectionTypeLoadExceptions.
Is there a correct way to load referenced assemblies?
I have tried LoadFile, LoadFrom and ReflectionOnlyLoadFrom all with the same result.
How do I get the types for methods that use a references Type?
I can step around the ReflectionTypeLoadExceptions error with the answer from here but these methods are the exact ones I want to map
TestLibraryA.dll
namespace TestLibraryA
{
public class TestClassA
{
public int DoStuff(int a, int b)
{
return a + b;
}
}
}
TestLibaryB.dll
using TestLibraryA;
namespace TestLibraryB
{
public class TestClassB
{
public int DoStuffAgain()
{
TestClassA obj = new TestClassA();
int ans = obj.DoStuff(3, 5);
return ans;
}
public TestClassA DoOtherStuff()
{
TestClassA result = new TestClassA();
return result;
}
}
}
Parser Code Application
public List<string> GetMethods()
{
List<string> result = new List<string> { };
Assembly dependencyAssembly = Assembly.LoadFile("TestLibraryA.dll");
Assembly targetAssembly = Assembly.LoadFile("TestLibaryB.dll");
Type[] types = targetAssembly.GetTypes();
// ReflectionTypeLoadExceptions thrown if a dependency type is used
// NB Not demo'ed in this example
foreach(var type in types)
{
foreach(var method in type.GetMethods())
{
// With the above DoStuffAgain() method is returned but DoOtherStuff() is not
var instructions = MethodBodyReader.GetInstructions(method);
// FileNotFoundException thrown saying TestLibraryA.dll not loaded
// the line throwing the error is
// MethodBodyReader(method)
// this.body = method.GetMethodBody();
foreach (var instruction in instructions)
{
MethodInfo methodInfo = instruction.Operand as MethodInfo;
if (methodInfo != null)
{
result.Add(methodInfo.DeclaringType.FullName + "." + methodInfo.Name);
}
}
}
}
return result;
}
To avoid the exception you need to add the code below before you load TestLibaryB
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
if (args.Name == "TestLibaryA...")
{
return Assembly.LoadFrom("TestLibaryA's Path");
}
return null;
}
I am trying to log the connected scanner on a pc.
I am using NTwain.dll from https://bitbucket.org/soukoku/ntwain.
If I run my app on a server some dependency dlls from ntwain fail to load so I will load the dll at runtime and if it will fail I just want to return an empty list. There is no reference to NTwain in the project references anymore.
Problem:
If I have the NTwain.dll in the folder with the exe and I run it on a server the app crashes. It doesn't return an empty list. If I delete the dll and run the app the empty list gets returned.
Code:
public class Scanner : IDB
{
private enum DataGroups : uint
{
None = 0,
Control = 0x1,
Image = 0x2,
Audio = 0x4,
Mask = 0xffff,
}
public string Name { get; private set; }
public string ProductFamily { get; private set; }
public string Version { get; private set; }
public Scanner()
{
Name = String.Empty;
}
public static List<Scanner> getScanners()
{
List<Scanner> scanners = new List<Scanner>();
try
{
Assembly assembly = Assembly.LoadFrom(Environment.CurrentDirectory + "\\NTwain.dll");
Type tident = assembly.GetType("NTwain.Data.TWIdentity");
Type tsession = assembly.GetType("NTwain.TwainSession");
object appId = tident.GetMethod("CreateFromAssembly").Invoke(null, new object[] { DataGroups.Image, System.Reflection.Assembly.GetExecutingAssembly() });
object session = Activator.CreateInstance(tsession, appId);
tsession.GetMethod("Open", new Type[0]).Invoke(session, null);
object sources = session.GetType().GetMethod("GetSources").Invoke(session, null);
foreach (var item in (IEnumerable)sources)
{
Scanner scanner = new Scanner();
scanner.Name = (string)item.GetType().GetProperty("Name").GetValue(item, null);
scanner.ProductFamily = (string)item.GetType().GetProperty("ProductFamily").GetValue(item, null);
object version = item.GetType().GetProperty("Version").GetValue(item, null);
scanner.Version = (string)version.GetType().GetProperty("Info").GetValue(version, null);
scanners.Add(scanner);
}
return scanners;
}
catch (Exception e)
{
return new List<Scanner>();
}
}
}
I would guess to catch a DllNotFoundException works in your code:
If u delete the dll then
Assembly assembly = Assembly.LoadFrom(Environment.CurrentDirectory + "\\NTwain.dll");
throws an DllNotFoundException which is catched by your catch block which says to return an empty list (which works as u say).
If u don't delete the dll then the code passes above line successfully. In case by the following lines a different thread is started and an error occurs that is not catched within that thread, then your catch block will not catch that error (whatever it might be) and the application crashes.
I want to use a custom path for a user.config file, rather than have .NET read it from the default location.
I am opening the file like this:
ExeConfigurationFileMap configMap = new ExeConfigurationFileMap();
configMap.ExeConfigFilename = String.Format("{0}\\user.config",AppDataPath);
Configuration config = ConfigurationManager.OpenMappedExeConfiguration(configMap, ConfigurationUserLevel.PerUserRoamingAndLocal);
But I can't figure out how to actually read settings out of it, I get a compile error saying that the values are inaccessible when I try to get a value through AppData or ConfigurationSection.
Do I need to create some sort of a wrapper class to consume the data properly?
I was recently tasked with a similar problem, I had to change the location of where settings files were read from the default location in AppData to the Application directory. My solution was to create my own settings files that derived from ApplicationSettingsBase which specified a custom SettingsProvider. While the solution felt like overkill at first, I've found it to be more flexible and maintainable than I had anticipated.
Update:
Sample Settings File:
public class BaseSettings : ApplicationSettingsBase
{
protected BaseSettings(string settingsKey)
{ SettingsKey = settingsKey.ToLower(); }
public override void Upgrade()
{
if (!UpgradeRequired)
return;
base.Upgrade();
UpgradeRequired = false;
Save();
}
[SettingsProvider(typeof(MySettingsProvider)), UserScopedSetting]
[DefaultSettingValue("True")]
public bool UpgradeRequired
{
get { return (bool)this["UpgradeRequired"]; }
set { this["UpgradeRequired"] = value; }
}
}
Sample SettingsProvider:
public sealed class MySettingsProvider : SettingsProvider
{
public override string ApplicationName { get { return Application.ProductName; } set { } }
public override string Name { get { return "MySettingsProvider"; } }
public override void Initialize(string name, NameValueCollection col)
{ base.Initialize(ApplicationName, col); }
public override void SetPropertyValues(SettingsContext context, SettingsPropertyValueCollection propertyValues)
{
// Use an XmlWriter to write settings to file. Iterate PropertyValueCollection and use the SerializedValue member
}
public override SettingsPropertyValueCollection GetPropertyValues(SettingsContext context, SettingsPropertyCollection props)
{
// Read values from settings file into a PropertyValuesCollection and return it
}
static MySettingsProvider()
{
appSettingsPath_ = Path.Combine(new FileInfo(Application.ExecutablePath).DirectoryName, settingsFileName_);
settingsXml_ = new XmlDocument();
try { settingsXml_.Load(appSettingsPath_); }
catch (XmlException) { CreateXmlFile_(settingsXml_); } //Invalid settings file
catch (FileNotFoundException) { CreateXmlFile_(settingsXml_); } // Missing settings file
}
}
A few improvements:
1) Load it up a bit simpler, no need for the other lines:
var config = ConfigurationManager.OpenExeConfiguration(...);
2) Access AppSettings properly:
config.AppSettings.Settings[...]; // and other things under AppSettings
3) If you want a custom configuration section, use this tool: http://csd.codeplex.com/
I never ended up getting the Configuration Manager approach working. After spending a half day muddling with no progress, I decided to roll my own solution as my needs are basic.
Here is the solution I came up with in the end:
public class Settings
{
private XmlDocument _xmlDoc;
private XmlNode _settingsNode;
private string _path;
public Settings(string path)
{
_path = path;
LoadConfig(path);
}
private void LoadConfig(string path)
{
//TODO: add error handling
_xmlDoc = null;
_xmlDoc = new XmlDocument();
_xmlDoc.Load(path);
_settingsNode = _xmlDoc.SelectSingleNode("//appSettings");
}
//
//use the same structure as in .config appSettings sections
//
public string this[string s]
{
get
{
XmlNode n = _settingsNode.SelectSingleNode(String.Format("//add[#key='{0}']", s));
return n != null ? n.Attributes["value"].Value : null;
}
set
{
XmlNode n = _settingsNode.SelectSingleNode(String.Format("//add[#key='{0}']", s));
//create the node if it doesn't exist
if (n == null)
{
n=_xmlDoc.CreateElement("add");
_settingsNode.AppendChild(n);
XmlAttribute attr =_xmlDoc.CreateAttribute("key");
attr.Value = s;
n.Attributes.Append(attr);
attr = _xmlDoc.CreateAttribute("value");
n.Attributes.Append(attr);
}
n.Attributes["value"].Value = value;
_xmlDoc.Save(_path);
}
}
}
I have an application that stores a collection of objects in the user settings, and is deployed via ClickOnce. The next version of the applications has a modified type for the objects stored. For example, the previous version's type was:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
And the new version's type is:
public class Person
{
public string Name { get; set; }
public DateTime DateOfBirth { get; set; }
}
Obviously, ApplicationSettingsBase.Upgrade wouldn't know how to perform an upgrade, since Age needs to be converted using (age) => DateTime.Now.AddYears(-age), so only the Name property would be upgraded, and DateOfBirth would just have the value of Default(DateTime).
So I'd like to provide an upgrade routine, by overriding ApplicationSettingsBase.Upgrade, that would convert the values as needed. But I've ran into three problems:
When trying to access the previous version's value using ApplicationSettingsBase.GetPreviousVersion, the returned value would be an object of the current version, which doesn't have the Age property and has an empty DateOfBirth property (since it can't deserialize Age into DateOfBirth).
I couldn't find a way to find out from which version of the application I'm upgrading. If there is an upgrade procedure from v1 to v2 and a procedure from v2 to v3, if a user is upgrading from v1 to v3, I need to run both upgrade procedures in order, but if the user is upgrading from v2, I only need to run the second upgrade procedure.
Even if I knew what the previous version of the application is, and I could access the user settings in their former structure (say by just getting a raw XML node), if I wanted to chain upgrade procedures (as described in issue 2), where would I store the intermediate values? If upgrading from v2 to v3, the upgrade procedure would read the old values from v2 and write them directly to the strongly-typed settings wrapper class in v3. But if upgrading from v1, where would I put the results of the v1 to v2 upgrade procedure, since the application only has a wrapper class for v3?
I thought I could avoid all these issues if the upgrade code would perform the conversion directly on the user.config file, but I found no easy way to get the location of the user.config of the previous version, since LocalFileSettingsProvider.GetPreviousConfigFileName(bool) is a private method.
Does anyone have a ClickOnce-compatible solution for upgrading user settings that change type between application versions, preferably a solution that can support skipping versions (e.g. upgrading from v1 to v3 without requiring the user to in install v2)?
I ended up using a more complex way to do upgrades, by reading the raw XML from the user settings file, then run a series of upgrade routines that refactor the data to the way it's supposed to be in the new next version. Also, due to a bug I found in ClickOnce's ApplicationDeployment.CurrentDeployment.IsFirstRun property (you can see the Microsoft Connect feedback here), I had to use my own IsFirstRun setting to know when to perform the upgrade. The whole system works very well for me (but it was made with blood and sweat due to a few very stubborn snags). Ignore comments mark what is specific to my application and is not part of the upgrade system.
using System;
using System.Collections.Specialized;
using System.Configuration;
using System.Xml;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using System.Reflection;
using System.Text;
using MyApp.Forms;
using MyApp.Entities;
namespace MyApp.Properties
{
public sealed partial class Settings
{
private static readonly Version CurrentVersion = Assembly.GetExecutingAssembly().GetName().Version;
private Settings()
{
InitCollections(); // ignore
}
public override void Upgrade()
{
UpgradeFromPreviousVersion();
BadDataFiles = new StringCollection(); // ignore
UpgradePerformed = true; // this is a boolean value in the settings file that is initialized to false to indicate that settings file is brand new and requires upgrading
InitCollections(); // ignore
Save();
}
// ignore
private void InitCollections()
{
if (BadDataFiles == null)
BadDataFiles = new StringCollection();
if (UploadedGames == null)
UploadedGames = new StringDictionary();
if (SavedSearches == null)
SavedSearches = SavedSearchesCollection.Default;
}
private void UpgradeFromPreviousVersion()
{
try
{
// This works for both ClickOnce and non-ClickOnce applications, whereas
// ApplicationDeployment.CurrentDeployment.DataDirectory only works for ClickOnce applications
DirectoryInfo currentSettingsDir = new FileInfo(ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoamingAndLocal).FilePath).Directory;
if (currentSettingsDir == null)
throw new Exception("Failed to determine the location of the settings file.");
if (!currentSettingsDir.Exists)
currentSettingsDir.Create();
// LINQ to Objects for .NET 2.0 courtesy of LINQBridge (linqbridge.googlecode.com)
var previousSettings = (from dir in currentSettingsDir.Parent.GetDirectories()
let dirVer = new { Dir = dir, Ver = new Version(dir.Name) }
where dirVer.Ver < CurrentVersion
orderby dirVer.Ver descending
select dirVer).FirstOrDefault();
if (previousSettings == null)
return;
XmlElement userSettings = ReadUserSettings(previousSettings.Dir.GetFiles("user.config").Single().FullName);
userSettings = SettingsUpgrader.Upgrade(userSettings, previousSettings.Ver);
WriteUserSettings(userSettings, currentSettingsDir.FullName + #"\user.config", true);
Reload();
}
catch (Exception ex)
{
MessageBoxes.Alert(MessageBoxIcon.Error, "There was an error upgrading the the user settings from the previous version. The user settings will be reset.\n\n" + ex.Message);
Default.Reset();
}
}
private static XmlElement ReadUserSettings(string configFile)
{
// PreserveWhitespace required for unencrypted files due to https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=352591
var doc = new XmlDocument { PreserveWhitespace = true };
doc.Load(configFile);
XmlNode settingsNode = doc.SelectSingleNode("configuration/userSettings/MyApp.Properties.Settings");
XmlNode encryptedDataNode = settingsNode["EncryptedData"];
if (encryptedDataNode != null)
{
var provider = new RsaProtectedConfigurationProvider();
provider.Initialize("userSettings", new NameValueCollection());
return (XmlElement)provider.Decrypt(encryptedDataNode);
}
else
{
return (XmlElement)settingsNode;
}
}
private static void WriteUserSettings(XmlElement settingsNode, string configFile, bool encrypt)
{
XmlDocument doc;
XmlNode MyAppSettings;
if (encrypt)
{
var provider = new RsaProtectedConfigurationProvider();
provider.Initialize("userSettings", new NameValueCollection());
XmlNode encryptedSettings = provider.Encrypt(settingsNode);
doc = encryptedSettings.OwnerDocument;
MyAppSettings = doc.CreateElement("MyApp.Properties.Settings").AppendNewAttribute("configProtectionProvider", provider.GetType().Name);
MyAppSettings.AppendChild(encryptedSettings);
}
else
{
doc = settingsNode.OwnerDocument;
MyAppSettings = settingsNode;
}
doc.RemoveAll();
doc.AppendNewElement("configuration")
.AppendNewElement("userSettings")
.AppendChild(MyAppSettings);
using (var writer = new XmlTextWriter(configFile, Encoding.UTF8) { Formatting = Formatting.Indented, Indentation = 4 })
doc.Save(writer);
}
private static class SettingsUpgrader
{
private static readonly Version MinimumVersion = new Version(0, 2, 1, 0);
public static XmlElement Upgrade(XmlElement userSettings, Version oldSettingsVersion)
{
if (oldSettingsVersion < MinimumVersion)
throw new Exception("The minimum required version for upgrade is " + MinimumVersion);
var upgradeMethods = from method in typeof(SettingsUpgrader).GetMethods(BindingFlags.Static | BindingFlags.NonPublic)
where method.Name.StartsWith("UpgradeFrom_")
let methodVer = new { Version = new Version(method.Name.Substring(12).Replace('_', '.')), Method = method }
where methodVer.Version >= oldSettingsVersion && methodVer.Version < CurrentVersion
orderby methodVer.Version ascending
select methodVer;
foreach (var methodVer in upgradeMethods)
{
try
{
methodVer.Method.Invoke(null, new object[] { userSettings });
}
catch (TargetInvocationException ex)
{
throw new Exception(string.Format("Failed to upgrade user setting from version {0}: {1}",
methodVer.Version, ex.InnerException.Message), ex.InnerException);
}
}
return userSettings;
}
private static void UpgradeFrom_0_2_1_0(XmlElement userSettings)
{
// ignore method body - put your own upgrade code here
var savedSearches = userSettings.SelectNodes("//SavedSearch");
foreach (XmlElement savedSearch in savedSearches)
{
string xml = savedSearch.InnerXml;
xml = xml.Replace("IRuleOfGame", "RuleOfGame");
xml = xml.Replace("Field>", "FieldName>");
xml = xml.Replace("Type>", "Comparison>");
savedSearch.InnerXml = xml;
if (savedSearch["Name"].GetTextValue() == "Tournament")
savedSearch.AppendNewElement("ShowTournamentColumn", "true");
else
savedSearch.AppendNewElement("ShowTournamentColumn", "false");
}
}
}
}
}
The following custom extention methods and helper classes were used:
using System;
using System.Windows.Forms;
using System.Collections.Generic;
using System.Xml;
namespace MyApp
{
public static class ExtensionMethods
{
public static XmlNode AppendNewElement(this XmlNode element, string name)
{
return AppendNewElement(element, name, null);
}
public static XmlNode AppendNewElement(this XmlNode element, string name, string value)
{
return AppendNewElement(element, name, value, null);
}
public static XmlNode AppendNewElement(this XmlNode element, string name, string value, params KeyValuePair<string, string>[] attributes)
{
XmlDocument doc = element.OwnerDocument ?? (XmlDocument)element;
XmlElement addedElement = doc.CreateElement(name);
if (value != null)
addedElement.SetTextValue(value);
if (attributes != null)
foreach (var attribute in attributes)
addedElement.AppendNewAttribute(attribute.Key, attribute.Value);
element.AppendChild(addedElement);
return addedElement;
}
public static XmlNode AppendNewAttribute(this XmlNode element, string name, string value)
{
XmlAttribute attr = element.OwnerDocument.CreateAttribute(name);
attr.Value = value;
element.Attributes.Append(attr);
return element;
}
}
}
namespace MyApp.Forms
{
public static class MessageBoxes
{
private static readonly string Caption = "MyApp v" + Application.ProductVersion;
public static void Alert(MessageBoxIcon icon, params object[] args)
{
MessageBox.Show(GetMessage(args), Caption, MessageBoxButtons.OK, icon);
}
public static bool YesNo(MessageBoxIcon icon, params object[] args)
{
return MessageBox.Show(GetMessage(args), Caption, MessageBoxButtons.YesNo, icon) == DialogResult.Yes;
}
private static string GetMessage(object[] args)
{
if (args.Length == 1)
{
return args[0].ToString();
}
else
{
var messegeArgs = new object[args.Length - 1];
Array.Copy(args, 1, messegeArgs, 0, messegeArgs.Length);
return string.Format(args[0] as string, messegeArgs);
}
}
}
}
The following Main method was used to allow the system to work:
[STAThread]
static void Main()
{
// Ensures that the user setting's configuration system starts in an encrypted mode, otherwise an application restart is required to change modes.
Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoamingAndLocal);
SectionInformation sectionInfo = config.SectionGroups["userSettings"].Sections["MyApp.Properties.Settings"].SectionInformation;
if (!sectionInfo.IsProtected)
{
sectionInfo.ProtectSection(null);
config.Save();
}
if (Settings.Default.UpgradePerformed == false)
Settings.Default.Upgrade();
Application.Run(new frmMain());
}
I welcome any input, critique, suggestions or improvements. I hope this helps someone somewhere.
This may not really be the answer you are looking for but it sounds like you are overcomplicating the problem by trying to manage this as an upgrade where you aren't going to continue to support the old version.
The problem isn't simply that the data type of a field is changing, the problem is that you are totally changing the business logic behind the object and need to support objects that have data relating to both old and new business logic.
Why not just continue to have a person class which has all 3 properties on it.
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public DateTime DateOfBirth { get; set; }
}
When the user upgrades to the new version, the age is still stored, so when you access the DateOfBirth field you just check if a DateOfBirth exists, and if it doesn't you calculate it from the age and save it so when you next access it, it already has a date of birth and the age field can be ignored.
You could mark the age field as obsolete so you remember not to use it in future.
If necessary you could add some kind of private version field to the person class so internally it knows how to handle itself depending on what version it considers itself to be.
Sometimes you do have to have objects that aren't perfect in design because you still have to support data from old versions.
I know this has already been answered but I have been toying with this and wanted to add a way I handled a similar (not the same) situation with Custom Types:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
private DateTime _dob;
public DateTime DateOfBirth
{
get
{
if (_dob is null)
{ _dob = DateTime.Today.AddYears(Age * -1); }
else { return _dob; }
}
set { _dob = value; }
}
}
If both the private _dob and public Age is null or 0, you have another issue all together. You could always set DateofBirth to DateTime.Today by default in that case. Also, if all you have is an individual's age, how will you tell their DateOfBirth down to the day?