Update dependent properties



  • So, I've already described my problem in the Lounge, but since I'm a little stuck, I might as well pull it out here.

    I have a bunch of objects representing some models. Said models have a lot of properties, and some of them should be calculated based on other properties - an example model would look like that:

    public class MyModel
    {
        public decimal Foo { get; set; }
        public decimal Bar { get; set; }
        public decimal Baz { get { return Bar * 2; } }
        public decimal Quux { get { return Bar + Baz; } }
        public decimal Norf { get { return Foo * 0.5; } }
    }
    

    Easy and simple. Problem is, the data in the DB sometimes will not follow the formulas, and the application needs to account for that:

    • each record needs a "Calculate" flag. If it's set, the fields should follow the formulas and automatically update themselves, but if it's not, then Baz, Quux and Norf should be editable.
    • if the fields are set from the DB, then the values in the fields should not change until a relevant field is edited. So if the user edits Bar, Baz and Quux should be recalculated based on the new value, but Norf should not even if it's not equal to Foo * 0.5 at the moment.

    I have this code, and it works:

    public class MyModel
    {
        public bool Calculate { get; set; }
        private decimal _foo;
        public decimal Foo
        {
            get { return _foo; }
            set
            {
                _foo = value;
                _norf = _norfCalculated;
            }
        }
    
        private decimal _bar;
        public decimal Bar
        {
            get { return _bar; }
            set
            {
                _bar = value;
                _baz = _bazCalculated;
                _quux = _quuxCalculated;
            }
        }
        private decimal _bazBacking;
        private decimal _baz //quux needs to be updated even if we're assigning privately
        {
            get { return _bazBacking; }
            set
            {
                 _bazBacking = value;             
                 _quux = _quuxCalculated;
            }
        }
        private decimal _bazCalculated { get { return Bar * 2; } }
        private decimal _bazUser;
        public decimal Baz
        {
            get { return Calculate ? _baz : _bazUser; }
            set
            {
                 _baz = value;
                 _bazUser = value;
            }
        }
    
        private decimal _quux;
        private decimal _quuxCalculated { get { return Bar + Baz; } }
        private decimal _quuxUser;
        public decimal Quux
        {
            get { return Calculate ? _quux : _quuxUser; }
            set
            {
                 _quux = value;
                 _quuxUser = value;
            }
        }
    
        private decimal _norf;
        private decimal _norfCalculated { get { return Foo * 0.5; } }
        private decimal _norfUser;
        public decimal Norf
        {
            get { return Calculate ? _norf : _norfUser; }
            set
            {
                 _norf = value;
                 _norfUser = value;
            }
        }
    }
    

    It works, but it's rather terrible to manage, given the fact that there can be anywhere between 30 to 70 properties on any given model, there are almost 20 models, and you basically need to reason backwards from the formulas - if Baz = Bar * 2, then you not only have to update Baz to calculate itself, but also remember to update Bar to trigger the property update. I tried the magic reflection wand, and it helped a bit, but not much:

    public class MyModel
    {
        private enum Fields //no magic strings in the application
        {
            Foo, Bar, Baz, Quux, Norf
        }
        
        private void UpdateDependents(Expression<Func<MyModel, object>> caller) //goes into the base class actually
        {
            var changedPropertyInfo = LambdaParser.GetPropertyByLambda(changedProperty); // gets PropertyInfo from the selector
            if (changedPropertyInfo == null) return;
            if (changedPropertyInfo.HasAttribute<FieldAttribute>())
            {
                var changedFieldEnum = changedPropertyInfo.GetAttribute<FieldAttribute>().Field; //get the field ID
                if (changedFieldEnum == null) return;
                var allMembers = typeof(MyModel)
                    .GetMembers(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); //get all members
                var dependentMembers = allMembers
                       .Where(x => x.GetAttributes<DependsOnAttribute>()
                                    .Any(y => changedFieldEnum.Equals(y.Field))); //get all members with a DependsOn attribute pointing at current field
                foreach (var dependentMember in dependentMembers)
                {
                    var dependentFieldEnum = dependentMember.GetAttribute<FieldAttribute>().Field; //get ID of the dependent field
                    var calculationField = allMembers
                           .Where(x => x.GetAttributes<CalculatingBackingFieldAttribute>()
                                        .Any(y => dependentFieldEnum.Equals(y.Field))).FirstOrDefault(); //find the backing field that does the calculation
                    var baseField = allMembers
                           .Where(x => x.GetAttributes<BaseBackingFieldAttribute>()
                                        .Any(y => dependentFieldEnum.Equals(y.Field))).FirstOrDefault(); //find the backing field to set
                    var value = ((PropertyInfo)calculationField).GetValue(this, null); //get the calculated value
                    ((FieldInfo)baseField).SetValue(this, value); //set the backing field to the calculated value
                }
            }
        }
    
        public bool Calculate { get; set; }
        private decimal _foo;
        [Field(Fields.Foo)]
        public decimal Foo
        {
            get { return _foo; }
            set
            {
                _foo = value;
                UpdateDependents(x => x.Foo);
            }
        }
    
        private decimal _bar;
        [Field(Fields.Bar)]
        public decimal Bar
        {
            get { return _bar; }
            set
            {
                _bar = value;
                UpdateDependents(x => x.Bar);
            }
        }
        [BaseBackingField(Fields.Baz)]
        private decimal _baz;
        [CalculatingBackingField(Fields.Baz)]
        private decimal _bazCalculated { get { return Bar * 2; } }
        private decimal _bazUser;
        [Field(Fields.Baz)]
        [DependsOn(Fields.Bar)]
        public decimal Baz
        {
            get { return Calculate ? _baz : _bazUser; }
            set
            {
                 _baz = value;
                 _bazUser = value;
                 UpdateDependents(x => x.Baz);
            }
        }
        [BaseBackingField(Fields.Quux)]
        private decimal _quux;
        [CalculatingBackingField(Fields.Quux)]
        private decimal _quuxCalculated { get { return Bar + Baz; } }
        private decimal _quuxUser;
        [Field(Fields.Quux)]
        [DependsOn(Fields.Bar)]
        [DependsOn(Fields.Baz)]
        public decimal Quux
        {
            get { return Calculate ? _quux : _quuxUser; }
            set
            {
                 _quux = value;
                 _quuxUser = value;
                 UpdateDependents(x => x.Quux);
            }
        }
        [BaseBackingField(Fields.Norf)]
        private decimal _norf;
        [CalculatingBackingField(Fields.Norf)]
        private decimal _norfCalculated { get { return Foo * 0.5; } }
        private decimal _norfUser;
        [Field(Fields.Norf)]
        [DependsOn(Fields.Foo)]
        public decimal Norf
        {
            get { return Calculate ? _norf : _norfUser; }
            set
            {
                 _norf = value;
                 _norfUser = value;
                 UpdateDependents(x => x.Norf);
            }
        }
    }
    

    On one hand, it inverts the problem and allows me to just set the attributes on the property to be updated, instead of modifying properties that trigger the updates. On the other, it's a total fucking incomprehensible spaghetti soup, it requires me to define an enum with every property involved in the mechanism (no, really, fuck magic strings), and it still requires me to remember to call UpdateDependents(). Performance will also likely suck, but at least that is not much of an issue.

    In short - I need a smart way to trigger property updates when a property on the model changes, and I can't use the standard calculated properties. What do I do?



  • Without actually knowing anything about C#, isn't this fixable with change listeners?



  • What I'd do:

    • Dictionary<Field, decimal> fixed_values that will hold fixed values
    • Dictionary<Field, Func<decimal>> formulas that will hold formulas to calculate dependencies

    When value of any field changes, iterate over all entries in formulas and assign results to calculated_values. A getter for each field should check if a fixed value exists for a given field - if yes, return it, if not, calculate it using formula.

    If you cannot afford to calculate each property again and again whenever it's used, you can cache the results in another dictionary - say, calculated_values - and on each change to any fixed value, iterate over all formulas and update respective entries in calculated_values regardless of whether there was a dependency between the values or not. In short - fuck dependencies, update everything. This is assuming that recalculating all the values on each change doesn't take too long, which I would be very surprised if it did, since we're talking about a hundred values tops.



  • @Gaska said:

    In short - fuck dependencies, update everything.

    Problem is, it's a business requirement that we don't update everything, just the values that actually depend - directly or indirectly - on the changed value. Otherwise it would be much simpler.



  • If a tree falls in a forest and no one is around to hear it, does it make a sound? If a value is recalculated and assigned the same value as before, has it really been updated?



  • @Gaska said:

    If a value is recalculated and assigned the same value as before, has it really been updated?

    Let's say you pull from the database:

    Foo |Bar |Baz |Quux|Norf|
    12  |34  |751 |9050|1267|
    

    Then, in the application, you assign Bar = 50. If you recalculate all the values, you get

    Foo |Bar |Baz |Quux|Norf|
    12  |50  |100 |150 |24  |
    

    Instead of:

    Foo |Bar |Baz |Quux|Norf|
    12  |50  |100 |150 |1267|
    

    which is what the business requires you to get, since OMG I haven't changed Foo and Norf changed under me, fix it nao!



  • You calculate all values, but use the calculated value only when there is no fixed value already present for the given field.



  • But Baz, Quux, and Norf already have a value in the DB, which overrides calculations, right?



  • @PleegWat said:

    But Baz, Quux, and Norf already have a value in the DB, which overrides calculations, right?

    Yeah, well, that would be simple.

    They have values in the DB, and those values are set on initialization, but when the user edits Bar later while the Calculate flag is set, it should recalculate the values for Baz and Quux based on the new Bar value.

    Which is also why @Gaska's method doesn't quite work - depending on how you look at it, either none or all of the properties have a "fixed value" at the beginning, and yet only Baz and Quux should be updated when Bar is, and Norf should be left alone.



  • OK I'm getting out of here before my head explodes.



  • @Maciejasjmj said:

    They have values in the DB, and those values are set on initialization, but when the user edits Bar later while the Calculate flag is set, it should recalculate the values for Baz and Quux based on the new Bar value.

    OK, I'm completely lost. Let's talk about concrete examples. Dependencies are Foo <- Bar <- Baz.

    1. Foo is initialized with value, Bar and Baz are not. When Foo changes, both Bar and Baz change?
    2. Foo and Bar are initialized with value, Baz is not. When Foo changes, neither Bar nor Baz change?
    3. Foo and Baz are initialized with value, Bar is not. Bar changes by some other means than changing Foo. Does it make sense at all? If so, does either Foo or Baz change?


  • @Gaska said:

    OK, I'm completely lost.

    Ok, let's start from scratch. There's no concept of "initialized/uninitialized", every field gets a starting value from the DB (which might be null, but then it's just null).

    Point is, the values the object is initialized with do not necessarily follow the formulas. That object is then shoved into a data-bound form. At this point, the values in the object are supposed to be the same as when it was pulled from the DB - if no changes were made, then we're supposed to save the object the same as it was before.

    Then, the user changes a value. When he does, if the Calculate flag is set, then every value dependent on the value changed should be calculated from the new value, but the values not dependent on the value changed should not be touched.

    For a more real life example: assume you have an object with the following fields:

    WalletInPLN | WalletInUSD | BankAcctInPLN | BankAcctInUSD | USDPLNRate
    2150        | 99999       | 10000         | 74344         | 2.00
    

    Let's say the formulas are WalletInUSD = WalletInPLN * USDPLNRate and BankAcctInUSD = BankAcctInPLN * USDPLNRate.

    The values you have in the object are what you pulled from the DB. If you don't change anything and just click "Save", the USD values shouldn't change, even though they're incorrect based on the exchange rate.

    If the user edits only WalletInPLN, then WalletInUSD should be updated based on the exchange rate, but the bank account values shouldn't be touched. So, if you update WalletInPLN = 2000, you should end up with the following values:

    WalletInPLN | WalletInUSD | BankAcctInPLN | BankAcctInUSD | USDPLNRate
    2000        | 4000 (calc.)| 10000         | 74344         | 2.00
    


  • Oh, I see. Previously I thought that Calculate is based solely on existence in DB.

    My previous solution can be adapted quite easily.

    • Dictionary<Field, decimal> values - will contain values, either calculated or fixed.
    • Dictionary<Field, Func> formulas - will contain formulas to recalculate.
    • Dictionary<Field, Field> dependencies - a hardcoded constant that will contain all edges in the dependency graph.
    • Dictionary<Field, Field> realDependencies - calculated based on dependencies; it contains all direct and indirect dependencies.
    • Set<Field> fieldsToRecalculate - will hold all fields that can be recalculated and don't have to match DB values
    • Initially, values is filled with DB values and fieldsToRecalculate is empty.
    • Getters look like:
    if (!values.has(field)) {
        values[field] = formulas[field](this);
    }
    return values[field];
    
    • If some field is changed, then all dependent values that are included in fieldsToRecalculate are removed from values, then they're each assigned recalculated value. The order in which they're recalculated in this step doesn't matter, since if some value is recalculated earlier than its dependency, the getter will get the job done.

    If you want a really fancy solution, you can automatically generate dependency graph by making a mock class which will remember which getters were called and return dummy values for them - it doesn't matter what, because the result won't be used. Then iterate over formulas, call each function with a new mock, and collect the results. Voila!



  • I don't really have a better solution (and I think reflection is icky), except I'd implement your original plan using nullable values:

    private decimal? _baz = null;
        
    public decimal Baz {
        get { if( _baz.HasValue() ) { return _baz.Value; } else { return Bar * 2; } }
        set { _baz = value }
    }
    

    Reflection just makes it nearly impossible to tell what's going on.


  • Impossible Mission Players - A

    Wow, 14 replies and not a single like? Where's the love :interrobang:



  • @Gaska said:

    Voila!

    I'm not sure if that's gonna result in less or more boilerplate than I have now, but hell, it might work, and I use a similar dictionary-based approach somewhere else so it won't be too out-of-place... will investigate Monday.

    @blakeyrat said:

    except I'd implement your original plan using nullable values:

    For some properties null can be a valid value, though. And your code doesn't quite seem to do what I need it to do - I'll need to fire the setter to load the values from the DB, so the else branch won't run once those are loaded.

    @blakeyrat said:

    Reflection just makes it nearly impossible to tell what's going on.

    Eh, it's a tool like any other. Used right it cuts on a lot of boilerplate. The example here is one of the nastier I've (not yet) commited, though.



  • @Maciejasjmj said:

    I'm not sure if that's gonna result in less or more boilerplate than I have now, but hell, it might work, and I use a similar dictionary-based approach somewhere else so it won't be too out-of-place... will investigate Monday.

    The only PITA I can think of is the code of all those properties. It would be terrible, terrible copypasta. But if C# has some kind of macros, or another way to mass-define properties with similar getters/setters, it shouldn't be that bad.



  • @Maciejasjmj said:

    For some properties null can be a valid value, though.

    Fair enough.

    @Maciejasjmj said:

    And your code doesn't quite seem to do what I need it to do - I'll need to fire the setter to load the values from the DB, so the else branch won't run once those are loaded.

    I thought the entire point was that if the values get loaded from the database, they should not be auto-calculated from the other values. I guess I misunderstood. Now I honestly don't know what you were expecting the code to do.

    @Maciejasjmj said:

    Eh, it's a tool like any other.

    It's only valid use in C# is building development tools, not code that's the functional part of your project.



  • @blakeyrat said:

    Now I honestly don't know what you were expecting the code to do.

    https://what.thedailywtf.com/t/update-dependent-properties/52684/12?u=maciejasjmj

    This post is, I think, the best explanation I can come up with.

    @blakeyrat said:

    It's only valid use in C# is building development tools, not code that's the functional part of your project.

    What about writing your own attributes? You have to drop down to reflection for that, and it can be really helpful for some tasks.



  • @Maciejasjmj said:

    This post is, I think, the best explanation I can come up with.

    I think I get it, the inclusion of "null" as a legit value just makes the solution more stupid. You basically need your own version of Nullable<T> which keeps track of not just whether the value is null, but also keeps track of whether it came from the database or not. NullableWithSource<T>

    To defend myself however: I would like to note that YOUR VERY OWN EXAMPLE THAT YOU PUT UP AT THE TOP OF THE THREAD uses "decimal" as the type; decimal is a value type and thus can not be null. If "null" is a valid result from the getter, your own code is wrong too. So there.

    @Maciejasjmj said:

    What about writing your own attributes? You have to drop down to reflection for that, and it can be really helpful for some tasks.

    Tools, like serializers, localization stuff, or test frameworks, use attributes. That's fine; those are utility libraries, they are not part of your project's code. (Even if it's a utility library you wrote yourself and you use only in one project and you don't distribute.)

    Your project's code should only contain the things your program does.

    This is all IMO of course.



  • @blakeyrat said:

    I would like to note that YOUR VERY OWN EXAMPLE THAT YOU PUT UP AT THE TOP OF THE THREAD uses "decimal" as the type

    Yes, I haven't thought of that when writing the example.

    @blakeyrat said:

    You basically need your own version of Nullable<T> which keeps track of not just whether the value is null, but also keeps track of whether it came from the database or not.

    The biggest hurdle is that when you set one of the values, some of the values should also be updated alongside it. So not just "keep track if the user changed this property", but also "keep track if the user changed another property that this property calculates its value from".

    @blakeyrat said:

    That's fine; those are utility libraries, they are not part of your project's code.

    So basically "wrap this shit up under the covers"? Yep, I could agree with that.



  • @Maciejasjmj said:

    Yes, I haven't thought of that when writing the example.

    Feh.

    @Maciejasjmj said:

    The biggest hurdle is that when you set one of the values, some of the values should also be updated alongside it.

    Just have the Setter for Bar blank-out the values dependent on Bar, however your NullableWithSource<T> class does that. Next time someone runs the Getter, they'll get the correct value.

    Look, no matter what happens you have a base level of complexity in this class. You can't just abstract it away-- at some point, some part of your program is going to have to say "if you update Bar, set Baz to null". You can either write 57 layers of abstraction and reflection and hide everything until it all breaks down, and then it's impossible to debug or-- you can just do:

    public Bar { set: { Baz = null; Bar = value; } };
    

    I mean do what you do, but I know which option I prefer. Look, the knowledge "changing Bar also requires resetting the value of Baz" is RIGHT THERE ON THE SCREEN! I LOVE IT! You didn't even have to use the phrase "dependency graph" at any point!

    @Maciejasjmj said:

    So basically "wrap this shit up under the covers"?

    I wouldn't phrase it that way. But sure why not.

    The real problem with Reflection is that you're moving all these potential compile-time errors and making them into potential run-time errors. Well, run-time errors suck. They're hard to track down, harder to fix, there's a chance your QA people won't even ever come across the condition to cause them and the poor user is going to see it first. Nobody wants their users to be, like, second-line QA.

    Then again, .net has been spending the last like 3 major releases doing almost nothing but converting compile-time errors into run-time errors. Sigh.



  • @blakeyrat said:

    Look, the knowledge "changing Bar also requires resetting the value of Baz" is RIGHT THERE ON THE SCREEN! I LOVE IT!

    With the drawback that when you're writing Bar, it requires you to remember that it resets the value of Baz. So if your next rule is "the value of Foo depends on Baz, Quux and Norf", you have to modify Baz, Quux and Norf to reset Foo when they're modified. And if you forget about it, it fails silently.

    Which in the end is probably the approach I'll end up with, but I wonder if there's a way to just have all properties that Foo's value depends on be specified in Foo, not hunt them around a 70-property object.

    @blakeyrat said:

    The real problem with Reflection is that you're moving all these potential compile-time errors and making them into potential run-time errors.

    If you're approaching it sanely, you can have compile-time checking just fine for the most part with only the (hopefully small, helper-like) code that directly uses reflection needing to be 100% correct and checked. Unless somebody does stupid shit like pass x => 42 where you expect a property selector, but then they only have themselves to blame.



  • @Maciejasjmj said:

    With the drawback that when you're writing Bar, it requires you to remember that it resets the value of Baz. So if your next rule is "the value of Foo depends on Baz, Quux and Norf", you have to modify Baz, Quux and Norf to reset Foo when they're modified. And if you forget about it, it fails silently.

    Well, ok, but that's the point: that knowledge has to be somewhere.

    If you use Gaska's idea with all of those dictionaries of functions, you're equally as likely to forget to update your code. So if you're debating between the two options, that particular scenario doesn't influence the decision at all.

    @Maciejasjmj said:

    Which in the end is probably the approach I'll end up with, but I wonder if there's a way to just have all properties that Foo's value depends on be specified in Foo, not hunt them around a 70-property object.

    A better approach would be to rethink your data model so you don't need a class like this in the first place. I didn't mention that before, because usually when people have questions like yours they aren't in charge of the DB schema and so that's not an option.



  • @blakeyrat said:

    Well, ok, but that's the point: that knowledge has to be somewhere.

    Yeah, but the point is to keep it in one place instead of strewn all over the model. A perfect solution would be if I could just update the formula and have some generic code handle everything, but that seems to be not quite possible with C#'s tooling.

    @blakeyrat said:

    I didn't mention that before, because usually when people have questions like yours they aren't in charge of the DB schema and so that's not an option.

    I'm not. And anyway, I have no idea what those 70 properties mean or how to group them, and the object actually looks like that from the business perspective, and they do want a 70-column grid to edit this object, so... well.



  • @Maciejasjmj said:

    Yeah, but the point is to keep it in one place instead of strewn all over the model. A perfect solution would be if I could just update the formula and have some generic code handle everything, but that seems to be not quite possible with C#'s tooling.

    Well, hell, do it in Excel then.



  • @blakeyrat said:

    If you use Gaska's idea with all of those dictionaries of functions, you're equally as likely to forget to update your code.

    Not if he goes for autogeneration, like I proposed :trolleybus:



  • C# events come to mind.



  • So, I was bored, and this looked like quite interesting challenge, so I made a basic implementation of my idea. 101 lines of T4 template (generated 325 for 7 properties), plus 48 lines of dependency... um... handler. Plus a quick and dirty example application hastily put together in WinForms.

    I think the result is pretty good. New property is one-liner - adding one entry in dictionary at the top of template file with name and expression to use for calculation. All the dependencies are derived straight from this expression, without any further boilerplate. All values are decimal and properties are identified by string, not enum, but both of these shouldn't be too hard to alter.

    I did my best to remove junk files from solution before uploading, but I'm not very good at it so there might be some leftovers.

    Autoproperty.7z (8.7 KB)



  • So I thought of implementing @Gaska's solution with a dependency graph, and then thought "why would I need to specify dependencies when they're evident from the formulas?" So I wrote myself a little utility class:

    public class FormulaDictionary<TObject> : List<FormulaDictionary<TObject>.FormulaObject<TObject>>
        {
            public class FormulaObject<T>
            {
                public PropertyInfo Property;
                public LambdaExpression Formula;
                public Func<T, object> CompiledFormula;
     
                private FormulaObject() { }
     
                public static FormulaObject<T> GetFormula<TResult>(Expression<Func<T, TResult>> propertySelector, Expression<Func<T, TResult>> formula)
                {
                    return new FormulaObject<T>
                    {
                        Property = LambdaParser.GetPropertyByLambda(propertySelector),
                        Formula = formula,
                        CompiledFormula = x => (object)((formula.Compile())(x))
                    };
                }
            }
     
            private class PropertyExtractingExpressionVisitor : ExpressionVisitor
            {
                public List<PropertyInfo> PrimaryProperties = new List<PropertyInfo>();
     
                protected override Expression VisitMember(MemberExpression node)
                {
                    if (node.Member is PropertyInfo)
                    {
                        PrimaryProperties.Add((PropertyInfo)node.Member);
                    }
                    return node;
                }
            }
     
            private Dictionary<string, List<PropertyInfo>> _primaryToDependent = new Dictionary<string, List<PropertyInfo>>();
            private Dictionary<string, FormulaObject<TObject>> _formulas = new Dictionary<string, FormulaObject<TObject>>();
     
            public void Add<T>(Expression<Func<TObject, T>> propertySelector, Expression<Func<TObject, T>> formula)
            {
                var formulaObject = FormulaObject<TObject>.GetFormula<T>(propertySelector, formula);
                var visitor = new PropertyExtractingExpressionVisitor();
                visitor.Visit(formulaObject.Formula);
                foreach (var primary in visitor.PrimaryProperties)
                {
                    if (!_primaryToDependent.ContainsKey(primary.Name)) _primaryToDependent[primary.Name] = new List<PropertyInfo>();
                    if (!(_primaryToDependent[primary.Name].Any(x => x.Name == formulaObject.Property.Name)))
                    {
                        _primaryToDependent[primary.Name].Add(formulaObject.Property);
                    }
                }
                _formulas[formulaObject.Property.Name] = formulaObject;
                this.Add(formulaObject);
            }
     
            public List<PropertyInfo> GetDependentProperties(string primaryName)
            {
                return _primaryToDependent.ContainsKey(primaryName) ? _primaryToDependent[primaryName] : new List<PropertyInfo>();
            }
     
            public bool HasFormula(string propertyName)
            {
                return _formulas.ContainsKey(propertyName);
            }
     
            public object GetCalculatedValue(TObject target, string propertyName)
            {
                return _formulas[propertyName].CompiledFormula(target);
            }
        }
    

    I'll need to comment over it later, but the basics are simple - when you add a selector-lambda pair, it parses out all property references from the lambda and makes a hashmap from property names to all the properties that need to be changed. Then all that one needs to do is to make a base class for the model:

    public abstract class BaseModel<TSelf>
        where TSelf : BaseModel<TSelf> //curiously recurring template pattern?
    {
        protected abstract FormulaDictionary<TSelf> Formulas { get; }
        public abstract bool Calculate { get; set; }
    
        private Dictionary<string, object> _propertyBackingValues = new Dictionary<string,object>();
     
            protected T GetValue<T>(Expression<Func<TSelf, T>> selector)
            {
                var propertyName = LambdaParser.LambdaToName(selector); // turns a (x => x.property) selector into "property" string
                return _propertyBackingValues.ContainsKey(propertyName) ? (T)_propertyBackingValues[propertyName] : default(T);
            }
     
            private void SetBackingValue(string propertyName, object value)
            {
                _propertyBackingValues[propertyName] = value;
                foreach (var dependentProperty in Formulas.GetDependentProperties(propertyName))
                {
                    SetBackingValue(dependentProperty.Name, Formulas.GetCalculatedValue((TSelf)this, dependentProperty.Name));
                }
            }
     
            protected void SetValue<T>(Expression<Func<TSelf, T>> selector, T value)
            {
                var propertyName = LambdaParser.LambdaToName(selector);            
                if (Calculate)
                {
                    SetBackingValue(propertyName, value);
                }
                else
                {
                    _propertyBackingValues[propertyName] = value;
                }
            }
    }
    

    And then creating the model is really simple:

    public class MyModel : BaseModel<MyModel>
    {
        private static FormulaDictionary<MyModel> _formulas = new FormulaDictionary<MyModel>
        {
            {x => x.Baz, x => x.Bar * 2},
            {x => x.Quux, x => x.Bar + x.Baz},
            {x => x.Norf, x => x.Foo * 0.5}
        };
    
        protected override FormulaDictionary<MyModel> Formulas { get { return _formulas; } }
        
        public override Calculate { get { return GetValue(x => x.Calculate); } set { SetValue(x => x.Calculate, value); } }
        
        public decimal Foo { get { return GetValue(x => x.Foo); } set { SetValue(x => x.Foo, value); } }
        public decimal Bar { get { return GetValue(x => x.Bar); } set { SetValue(x => x.Bar, value); } }
        public decimal Baz { get { return GetValue(x => x.Baz); } set { SetValue(x => x.Baz, value); } }
        public decimal Quux { get { return GetValue(x => x.Quux); } set { SetValue(x => x.Quux, value); } }
        public decimal Norf { get { return GetValue(x => x.Norf); } set { SetValue(x => x.Norf, value); } }
    }
    

  • Impossible Mission Players - A

    :rainbow:s. :rainbow:s Everywhere!



  • Nobody's going to figure out what the hell that's doing 6 months from now.



  • @blakeyrat said:

    Nobody's going to figure out what the hell that's doing 6 months from now.

    But they'll know where to change shit that might actually need changing. All the complex stuff is hidden in a utility package, all that anyone maintaining the application will generally to know is how to use it, not how it works. And it's much easier to define the formulas and let the magic work itself than to painstakingly figure out what depends on what by hand, then copy and paste shit all over and inevitably miss something that's going to make the logic silently shit itself.

    Besides, it's straightforward, if advanced code. It uses a well-documented feature of the language, and while it could use a few comments that I didn't have in yet when posting it, I fail to see why it would be black magic.

    If my alternatives are "use some obscure language feature" and "make my code a fragile piece of shit that actually is unmanageable because everything needs to be changed in 20 places at once"... Well, I'll take an ExpressionVisitor anytime.



  • I believe I already provided you an example that met all of these requirements without needing reflection or any complex stuff whatsoever. Where the "formula magic" is done by simply calling setters in the right order.

    I mean, hey, do what you want.



  • @blakeyrat said:

    Where the "formula magic" is done by simply calling setters in the right order.

    Point is, somebody has to maintain that order. If the business rules change - and it's more likely than not - then you have to go over a bunch of classes and recheck an awful lot of properties by hand.

    To me, the primary characteristic of maintainable code is that it needs to be changed in as few places as possible. And that things that are likely to need to be changed - like adding a new model, or updating the business rules - can be done as effortlessly as possible. Sure, that means changing the base or utility classes will be this much harder, since that complexity isn't going away, but that's also much less likely to be necessary.



  • I don't think you understood my proposal at all.



  • @blakeyrat said:

    I don't think you understood my proposal at all.

    Well, maybe I didn't. As I understand it, you want the setters to straight away update the necessary properties. So if the rule is A = B + C, then B and C's setters need to update A to B + C.

    So:

    public decimal? A { get; set; }
    public decimal? B { get { ... } set { ...; A = B + C } }
    public decimal? C { get { ... } set { ...; A = B + C } }
    

    Now, let's say the rule changes to A = B*2. Now you need to change B's setter, and you also need to change C's setter to no longer update the value, even though the new rule doesn't even touch C. If that's what your proposal was, then it's a maintainability nightmare.



  • You have it exactly backwards. You do the logic on the GETTER not the setter.



  • That changes nothing WRT the maintainability aspect, though.



  • Yes it does?

    Look, I'm done with this thread. It's obvious people are either just misunderstanding on purpose, or wouldn't understand without an extensive code sample I don't have the time or inclination to write.



  • @blakeyrat said:

    You have it exactly backwards. You do the logic on the GETTER not the setter.

    @blakeyrat said:

    Look, no matter what happens you have a base level of complexity in this class. You can't just abstract it away-- at some point, some part of your program is going to have to say "if you update Bar, set Baz to null". You can either write 57 layers of abstraction and reflection and hide everything until it all breaks down, and then it's impossible to debug or-- you can just do:

    public Bar { set: { Baz = null; Bar = value; } };

    I mean do what you do, but I know which option I prefer. Look, the knowledge "changing Bar also requires resetting the value of Baz" is RIGHT THERE ON THE SCREEN! I LOVE IT! You didn't even have to use the phrase "dependency graph" at any point!

    Well, crap, now I really don't get it.

    Either way, you mean something like:

    private decimal? _a;
    public decimal? A { get { return <something> ? _a : B + C; } set { _a = value; } }
    public decimal? B { get; set; }
    public decimal? C { get; set; }
    

    ? How do you write <something> so that it returns a different value once B or C is set without any logic in the setter?



  • Sigh.

    Ok look.

    You've written a class so you can tell if a value is null because it's null in the DB, or if it's null because you haven't set it yet. We'll call that case "Unset". We'll also assume the variable's value is stored in a getter named .Value. So it follows the pattern of Nullable<T>, but just with an added "Unset" bool.

    Your getters look like this and I'm typing this directly into Discourse so don't bitch at me if I typo it or whatever:

    public decimal? A { get { if( _a.IsUnset() ) { return B + C; } else { return _a.Value; } } set { _a = value; } };
    public decimal? B { get { if( _b.IsUnset() ) { return D * D; } else { return _b.Value; } } set { _b = value; } };
    

    So you see the logic that A depends on B and C, and B depends on D is right there in the getters, and those "cascading dependencies" will work fine. (Assuming none of them are circular.)

    And if you change the logic of A so it now should return B * B, well, bam, there you go, you only have to change it in one place, done.



  • His requirements are back-asswards. _a has a value from the DB but may or may not need to be updated based on God knows what.



  • Ok well. Whatever. There's my code sample.



  • @PleegWat said:

    His requirements are back-asswards.

    Yeah, what he said. It would be more like, and I'm not sure if that would work but it's kinda conceptually close:

    public decimal? A
    {
        get { if ( _b.IsUnset() && _c.IsUnset() ) { return _a.Value; } else { return B + C; } }
        set { _a = value; }
    }
    

    Once the user sets B, A should update itself.

    Now that might work, maybe? I'd need to check.



  • As I understand, no, because _a should only be recalculated if B or C changed this session?

    Which may be a feasible solution as well.



  • This thread makes me glad we don't do backpatching of old data, and as we're in a product environment our answer to customer requests of 'How do I use your product to doctor the figures' is 'You don't'. Only exception is we allow retroactively changing the SLA targets in your KPI reports.


Log in to reply
 

Looks like your connection to What the Daily WTF? was lost, please wait while we try to reconnect.