Our “Extension Properties” Story

Late last year I made myself a promise that I would resuscitate my blog instead of just tweeting drivel.  I don’t particularly feel up (or self important enough) to the old 10 page missives of yore, so I’m just going to write about the project and OSS work I’m doing this year and see if any of it is useful or interesting.

I’ve built software in some capacity for a dozen years.  My first couple projects were exciting just because it was my first couple projects, but for the most part, I’ve always felt like everybody else worked on cooler projects than I did – until I started at Dovetail anyway.  Yesterday I (mostly) wrapped up a crucial subsystem for us on our story for “extension properties that I thought was cool enough to share.

We know from experience that our customers need to add some of their own fields to track their own special data in the system.  Here’s the facts, requirements, and assumptions we’ve made about these extension properties:

  1. We’re assuming that the extension properties will be completely set up by our professional services folks during the deployment product, so we don’t have to open up any magic way for the customers to change the database schema and screens on the fly.
  2. Our “professional services” guys sit 10 feet from me and they’re perfectly capable of some coding
  3. We really need an “Open / Closed” architecture.  By this I mean that we absolutely must be able to add these customer specific properties to their own installation without any impact or change to our core codebase.  Nothing destroys a small ISV like having to maintain customer specific
  4. The extension properties have to be editable on the screen
  5. The extension properties have to be available inside our rules engine
  6. I’d really, really like to reuse all our existing infrastructure for validation, persistence, and html conventions for these extension properties.  I’m lazy and I don’t want to write a lot of special infrastructure.

 

Step 1:  Adding Extends<T> to the Domain Model

My original thought was to just make the extension properties live in a name/value collection on all of our entities.  It’s a simple solution and the quickie NHibernate spike I did made it clear that the persistence wouldn’t be a problem.  However, we make use of a *lot* of conventions and validation policies in our application that utilize the type system and our validation attributes to drive behavior.  Using the name/value approach meant that I threw all that away.  Instead, I decided that the extension properties would just live in a separate class like this one that I use for testing:

    // This object would be attached to our

    // Site objects in our Domain Model

    public class SiteExtensions : Extends<Site>

    {

        [Required, ShowNew]

        public string ExtraString { get; set; }

        public int ExtraNumber { get; set; }

        public DateTime? ExtraDate { get; set; }

 

        // Fake property and a fake list I use to test

        // multi-level lists

        [ListValue("Year"), ShowNew]

        public string Year { get; set; }

 

        [ListValue("Make"), ShowNew]

        public string Make { get; set; }

 

        [ListValue("Model"), ShowNew]

        public string Model { get; set; }

    }

 

A couple notes about this class.  As you see, it inherits from the Extends<TEntity> class where TEntity is the type in our domain model that is being extended.  Extends<TEntity> is strictly a marker interface with no functionality.

    public class Extends<T> where T : DomainEntity

    {

    }

 

You might also notice that the SiteExtensions class is decorated with several attributes (Required, ShowNew, ListValue, etc.).  These attributes are mostly for our server side validation, but they also drive Html construction in the web pages and tie into client side validation with jQuery validation (more on this later).

 

Step 2:  Discovering Extension Properties on Application Startup

So far, so good, but now I need to hook the SiteExtensions objects to Site objects while keeping the SiteExtensions class in a totally separate assembly specific to that particular customer.  First, all of our domain model classes implement a layer supertype class called “DomainEntity,” so this becomes a natural way to connect our domain entities to their extension properties:

    [Serializable]

    public class DomainEntity : Entity, IValidated

    {

        public object ExtendedProperties { get; set; }

 

    }

Next, I need to discover the proper extension property class for each domain model type (if any) at runtime.  I decided to store that information in a simple static class called “ExtensionProperties” that acts as a well known place in the system to query for the proper type of extension class:

    public static class ExtensionProperties

    {

        private static readonly Cache<Type, Type> _types = new Cache<Type, Type>();

 

        public static void Register(Type entityType, Type extensionType)

        {

            _types[entityType] = extensionType;

        }

 

        public static void ClearAll()

        {

            _types.ClearAll();

        }

 

        public static bool HasExtensionFor(Type entityType)

        {

            return _types.Has(entityType);

        }

 

        public static Type ExtensionFor(Type type)

        {

            return _types[type];

        }

    }

 

Now that we’ve got ExtensionProperties, we need to discover the available extension properties at application startup and register them with ExtensionProperties.  Unsurprisingly, I used StructureMap to discover the extension property types.  First off, we’re doing customer extensions by placing their customizations in separate assemblies that are placed into our application bin.  At the moment, our convention is that these assemblies must contain “Extensions” somewhere in their assembly name to be picked up.  So knowing that assumption, I created an auto-registration policy in StructureMap that scans all the “Extensions” assemblies in our application base directory and discovers the Extends<T> types:

    public class ExtensionRegistry : Registry

    {

        public ExtensionRegistry()

        {

            Scan(x =>

            {

                ExtensionProperties.ClearAll();

                x.AssembliesFromApplicationBaseDirectory(assem => assem.GetName().Name.Contains(“Extensions”));

                x.Convention<ExtensionScanner>();

            });

        }

    }

 

    // This little guy just finds any type that inherits from

    // Extends<T> and registers that type against “T” in

    // our wellknown ExtensionProperties class

    public class ExtensionScanner : IRegistrationConvention

    {

        public void Process(Type type, Registry graph)

        {

            if (type.BaseType.IsGenericType && type.BaseType.GetGenericTypeDefinition() == typeof (Extends<>))

            {

                var entityType = type.BaseType.GetGenericArguments()[0];

                ExtensionProperties.Register(entityType, type);

            }

        }

    }

 

Step 3:  Persisting the little devils

The goal is definitely to make the properties on the extension classes persistent, and I’d really prefer not to write custom SQL for this one off need, so it’s time to teach NHibernate how to persist these extension types – if they exist.  The Extends<T> objects are really just part of the domain entity that they extend, so let’s just say we put the backing fields directly onto the proper table for each entity.  We *might* have a conflict with the extension properties and later versions of our product, so let’s make a little naming convention for these backing fields and say that all extension fields in the database have to be prepended with “x_” like this (yes kids, I *can* write Sql by hand if I absolutely have to):

BEGIN TRANSACTION

GO

ALTER TABLE dbo.Site ADD

    x_ExtraString nvarchar(50) NULL,

    x_ExtraNumber int NULL,

    x_ExtraDate datetime NULL,

        x_Year nvarchar(20) NULL,

        x_Make nvarchar(50) NULL,

        x_Model nvarchar(100) NULL

 

COMMIT

 

Now I need to alter our NHibernate mappings *if* a domain entity has an extension type attached to it.  Back in the bad old HBM.XML days this used to be a klooge, but Fluent NHibernate (FNH) made it very simple.  Let’s just treat the Extends<T> type as a “Component” on the main entity type.  We have our own base class that extends FNH’s ClassMap<T> called DomainMap<T> to specify NHibernate mappings with our own policies, so let’s just add a bit of code to that base class that will dynamically create a component mapping to the extension property type if it exists:

    public abstract class DomainMap<T> : ClassMap<T>, IDomainMap where T : DomainEntity

    {

        protected DomainMap()

        {

            // For every DomainEntity class, use the Id property

            // as the Primary Key / Object Identifier

            Id(x => x.Id).ColumnName(“id”).GeneratedBy.GuidComb();

            WithTable(typeof(T).Name);

            Map(x => x.LastModified);

            Map(x => x.Created);

 

            if (ExtensionProperties.HasExtensionFor(typeof(T)))

            {

                var componentType = typeof (ExtensionComponent<>)

                    .MakeGenericType(ExtensionProperties.ExtensionFor(typeof (T)));

 

                var component = Activator.CreateInstance(componentType) as IMappingPart;

                AddPart(component);

            }

        }

    }

 

 

    public class ExtensionComponent<T> : ComponentPart<T>

    {

        public ExtensionComponent()

            : base(ReflectionHelper.GetProperty<DomainEntity>(x => x.ExtendedProperties), false)

        {

            SetAttribute(“class”, typeof(T).AssemblyQualifiedName);

 

            // Assume that all properties are persistable

            typeof(T).GetProperties()

                .Where(x => x.DeclaringType == typeof(T))

                .Each(prop =>

                {

                    // See the “x_” prefix for the naming convention?

                    Map(prop, “x_” + prop.Name);

                });

        }

    }

The awesome thing about using an “internal DSL” like that in Fluent NHibernate is that you still have every single bit of the real language at your disposal.  The code above effectively extends our NHibernate mapping to persist all the properties of the Extends<T> objects whenever the host entity is persisted.  By doing this we largely make the extension property persistence transparent to the main code.

 

Step 3:  Validating the Extension Properties

This part is actually pretty simple, but it doesn’t really translate all that well to other codebases.  For validation we use a homegrown mini-framework I built years ago that lives (largely abandoned) in the ShadeTree repository on Google Code (it hasn’t changed a lot since this post on the Notification pattern).  This tooling allows you to mix validation between attributes and special methods on a Domain Entity.  Going back to our DomainEntity supertype, we make it implement the ShadeTree “IValidated” interface and in the single Validate(Notification) method, we do a transparent pass through to validate the extension properties:

    [Serializable]

    public class DomainEntity : Entity, IValidated

    {

 

        public virtual void Validate(Notification notification)

        {

            if (ExtendedProperties == null) return;

 

            Validator.ValidateObject(ExtendedProperties, notification);

        }

 

        public object ExtendedProperties { get; set; }

 

    }

 

Not really much to this.  Your mechanics will be different for other validation frameworks, but the principle is still the same.  Just validate the extension property object if it exists and add those messages to whatever your framework uses for its Notification.

 

Step 4:  Putting this stuff on the screen

Most of our online forms present the main fields in a pretty standard 2 column layout.  For the moment, we’re trying to get away with just adding any extension properties to the bottom of the two columns and hope that’s good enough for most clients.  Once Chad and I made that assumption I moved onto making a new Html helper that would dynamically add the labels and editable fields for any extension properties:

    <%=this.ExtensionFieldsForView()%>

 

This helper needs to discover the proper extension type, if any, and write a label/field pair for each property it discovers on that extension object:

        // Discover what extension type, if any, is valid for the ViewModel of this page

        public static HtmlTag ExtensionFieldsForView<TViewModel>(this IDovetailViewWithModel<TViewModel> view) where TViewModel : class

        {

            var form = buildFormFor(view);

            var entityModel = view.Model as EditEntityModel;

            return form == null ? HtmlTag.Empty() : form.WriteView(entityModel.Target);

        }

 

        private static IExtensionForm buildFormFor<TViewModel>(IDovetailViewWithModel<TViewModel> view) where TViewModel : class

        {

            var editModel = view.Model as EditEntityModel;

            if (editModel == null) return null;

 

            var entityType = editModel.EntityType;

            if (!ExtensionProperties.HasExtensionFor(entityType)) return null;

 

 

            // The Form “magic” happens in ExtensionForm<,>

            var extensionType = ExtensionProperties.ExtensionFor(entityType);

            var extenderFormType = typeof(ExtensionForm<,>).MakeGenericType(extensionType, entityType);

 

            return (IExtensionForm)view.Container.GetInstance(extenderFormType);

        }

One of my goals with the extension properties was to utilize our existing infrastructure for Html generation.  We have a large investment in the FubuMVC Html conventions and it makes our form creation quick and consistent.  In order to use the Html conventions with our extension properties, I use this utility class to mediate between the dynamically discovered extension properties and the Fubu Html conventions:

    public interface IExtensionForm

    {

        HtmlTag WriteNew();

        HtmlTag WriteView(DomainEntity entity);

    }

 

    public class ExtensionForm<T, TEntity> : IExtensionForm where T : Extends<TEntity>, new() where TEntity : DomainEntity

    {

        private readonly TagGenerator<T> _tags;

 

        public ExtensionForm(TagGenerator<T> tags)

        {

            _tags = tags;

 

            // This is consistent w/ our naming convention

            _tags.ElementPrefix = typeof (TEntity).Name;

        }

 

        private HtmlTag writeFields(string profileName, IEnumerable<PropertyInfo> properties)

        {

            _tags.SetProfile(profileName);

            return new HtmlTag(“dl”, x =>

            {

                x.AddClass(“details”);

 

                properties.Each(prop =>

                {

                    x.Add(“dt”).Text(LocalizationManager.GetHeader(prop));

 

                    var request = _tags.GetRequest(new FubuMVC.Core.Util.SingleProperty(prop));

                    HtmlTag inputTag = _tags.InputFor(request);

                    x.Add(“dd”).Child(inputTag);

                });

            });

        }

 

        public HtmlTag WriteNew()

        {

            _tags.Model = new T();

            return writeFields(TagProfile.DEFAULT, ExtensionFieldRegistry.NewProperties<T>());

        }

 

        public HtmlTag WriteView(DomainEntity entity)

        {

            _tags.Model = (T) entity.ExtendedProperties ?? new T();

            return writeFields(DovetailViewActivator.EDIT_PROFILE, ExtensionFieldRegistry.ViewProperties<T>());

        }

    }

 

TagGenerator<T> is the main service in FubuMVC.UI for generating HtmlTag’s by convention.  As you can probably surmise from the code above, our localization machinery can lookup the header name for a PropertyInfo (or Expression) and that’s how we build the labels.  The actual editor tag is completely generated by the FubuMVC machinery based on its policies that we’ve configured:

    public class DovetailHtmlConventions : HtmlConventionRegistry

    {

        public DovetailHtmlConventions()

        {

            validationAttributes();

            numbers();

            dates();

 

            Editors.Builder<ListValueDropdownBuilder>();

 

            Editors.IfPropertyIs<bool>().BuildBy(request => new CheckboxTag(request.Value<bool>())

                .Style(“width”, “auto !important”)

                .Attr(“value”, request.ElementId));

 

            Editors.Always.Modify((request, tag) =>

            {

                tag.Attr(“label”, request.Header());

                tag.Attr(“name”, request.ElementId);

            });

        }

 

        private void numbers()

        {

            Editors.IfPropertyIs<Int32>().Attr(“max”, Int32.MaxValue);

            Editors.IfPropertyIs<Int16>().Attr(“max”, Int16.MaxValue);

            Editors.IfPropertyIs<Int64>().Attr(“max”, Int64.MaxValue);

            Editors.IfPropertyTypeIs(t => t.IsIntegerBased()).AddClass(“integer”);

            Editors.IfPropertyTypeIs(t => t.IsFloatingPoint()).AddClass(“number”);

        }

 

        private void dates()

        {

            Editors.IfPropertyTypeIs(t => t.IsDateTime()).Modify(x =>

            {

                if (!x.HasMetaData(EditInPlaceBuilder.EDITABLE_ATTRIBUTE_NAME))

                {

                    x.AddClass(“DatePicker”);

                }

            });

            Editors.If(prop => prop.Accessor.InnerProperty.IsDateAndTime()).Modify(x =>

            {

                if (!x.HasMetaData(EditInPlaceBuilder.EDITABLE_ATTRIBUTE_NAME))

                {

                    x.AddClass(“time-picker”);

                }

            });

        }

 

        private void validationAttributes()

        {

            Editors.AddClassForAttribute<RequiredAttribute>(“required”);

            Editors.ModifyForAttribute<MaximumStringLengthAttribute>((tag, att) =>

            {

                if (att.Length < Entity.UnboundedStringLength)

                {

                    tag.Attr(“maxlength”, att.Length);

                }

            });

 

 

            Editors.ModifyForAttribute<GreaterOrEqualToZeroAttribute>(tag => tag.Attr(“min”, 0));

            Editors.ModifyForAttribute<GreaterThanZeroAttribute>(tag => tag.Attr(“min”, 1));

        }

    }

And of course, by using the normal Html conventions to build the Html input tags, we get the jQuery validation integration for our extension properties.

 

Oh, and because Reflection scanning can be expensive, we cache the PropertyInfo’s for each extension type:

    public static class ExtensionFieldRegistry

    {

        private static readonly Cache<Type, IEnumerable<PropertyInfo>> _newProps

            = new Cache<Type,IEnumerable<PropertyInfo>>(findNewProps);

 

        private static readonly Cache<Type, IEnumerable<PropertyInfo>> _viewProps

            = new Cache<Type,IEnumerable<PropertyInfo>>(findViewProps);

 

        private static IEnumerable<PropertyInfo> findNewProps(Type type)

        {

            return type.GetProperties().Where(x => x.DeclaringType == type && x.HasAttribute<ShowNewAttribute>());

        }

 

        private static IEnumerable<PropertyInfo> findViewProps(Type type)

        {

            return type.GetProperties().Where(x => x.DeclaringType == type);

        }

 

        public static IEnumerable<PropertyInfo> NewProperties<T>()

        {

            return _newProps[typeof (T)];

        }

 

        public static IEnumerable<PropertyInfo> ViewProperties<T>()

        {

            return _viewProps[typeof (T)];

        }

    }

 

Step 5:  Getting data from the browser to the server

One more step (we also do edit in place editing of the extension properties in the UI, but that code is a mess and I’m not showing it to anyone until it’s cleaner).  We have the html tags on the browser and NHibernate knows how to persist the fields.  All we have to do now is get the data from the browser and marshal it into the new extension property type on the server.  The ViewModels for submitting the “new” forms all inherit from this base type that, conveniently enough, has a property for the extension properties:

    public class UpdateEntityModel<T> : IItemRequest where T : DomainEntity

    {

        public Extends<T> ExtendedProperties { get; set; }

        public Guid Id{ get; set; }

    }

I’m omitting some (lots) of code here, but I used an extension point on FubuMVC’s model binding to “fill” that ExtendedProperties property above with this code:

    public class ExtensionPropertyBinder : IPropertyBinder

    {

        // This only applies to properties that close Extends<>

        public bool Matches(PropertyInfo property)

        {

            return property.PropertyType.Closes(typeof (Extends<>));

        }

 

        public void Bind(PropertyInfo property, IBindingContext context)

        {

            var entityType = property.PropertyType.GetGenericArguments()[0];

 

            // If there is no Extends<> for the entity type, do nothing

            if (!ExtensionProperties.HasExtensionFor(entityType))

            {

                return;

            }

 

            var extensionType = ExtensionProperties.ExtensionFor(entityType);

 

            // direct the FubuMVC model binding to resolve an object of the

            // extensionType using “entityType.Name” as the prefix on the form data,

            // and place the newly created object using the specified property

            context.BindChild(property, extensionType, entityType.Name);

        }

    }

 

 

Wrapping Up

We’ll see how it goes in the end, but I’m relatively happy with how it all turned out.  The end result is that we can just build a very simple class with properties and some attribute declarations, put that assembly in the right spot, and voila!, the right stuff appears on the screen.

Lots of stuff to throw at you, but I’ll try to answer any questions.

About Jeremy Miller

Jeremy is the Chief Software Architect at Dovetail Software, the coolest ISV in Austin. Jeremy began his IT career writing "Shadow IT" applications to automate his engineering documentation, then wandered into software development because it looked like more fun. Jeremy is the author of the open source StructureMap tool for Dependency Injection with .Net, StoryTeller for supercharged acceptance testing in .Net, and one of the principal developers behind FubuMVC. Jeremy's thoughts on all things software can be found at The Shade Tree Developer at http://codebetter.com/jeremymiller.
This entry was posted in FubuMVC, StructureMap. Bookmark the permalink. Follow any comments here with the RSS feed for this post.
  • Guest

    Any chance you rewrote this to Loquacious mappings?

  • Bert

    This works fine (with some adjustments) for Property Mappings, but what about HasMany mappings?

  • http://realfiction.net Frank Quednau

    As it seems, the current Fluent NH does not have any non-generic “AddPart” type method anymore. The way to go right now is outline here at (http://support.fluentnhibernate.org/discussions/help/34-conventionally-adding-components-dynamically), which is somewhat uglier. Incidentally, the rest of the code looks much like yours…I also had the feeling that this should be solvable based on IConvention stuff, but if I understand the comments in that support thread correctly it isn’t deemed possible right now…

  • sebastien

    I’m having a similar issue but my properties change on a row basis. I will have many different Extends depending the type of Site for example.
    So far, I end up using an xml column containing those extended properties and a column with the name of the Extends
    type to instanciate.
    Maybe extra column with a convention like x_subtype_propertyname would be better though. I was afraid of having to many column and too many sparse column.

    Any better suggestion on how to support this ?

  • http://codebetter.com/members/jmiller/default.aspx Jeremy D. Miller

    @Patrick,

    The database migration issue is a real problem, but then again, I’m never ever going to just run willy nilly into Sql Server and rename columns.

    This:

    ID|Table|TableID|PropertyName|PropertyValue

    is not something I’d ever recommend or want to use (been there, got the tee shirt, it sucked).

  • Patrick

    I do wonder how the database aspect of this will play out long term. Heres a scenario:

    If you rename a column in sql server 2005 and have management studio generate a script for the change, you will see that it will:

    1. Create a tmp table with the renamed field name
    2. Migrate all records from the production table to the tmp table.
    3. Rename the tmp table to the production table name.

    For all customers who have additional fields in that particular table, you will lose all of the additional field data (assuming you dont create a unique script for each customer). Im not saying this is the only way to rename a field through DDL, but it does illustrate added friction in versioning your schema.

    I don’t know enough about your domain, but I would probably model the table like…
    ID|Table|TableID|PropertyName|PropertyValue

    I understand this adds complexity to general queries, and you rely on the application to handle data type issues, but it keeps all of your customers on the same schema and I think that holds a lot of value.

  • http://smellegantcode.wordpress.com Daniel Earwicker

    This is excellent. The views have “extension” stuff, customer-specific addenda, which automatically links together database -> model -> presentation for each field. (Although I agree with Khalid that it’s a shame the DDL SQL is not also generated from the attributes – either that or store the extensions in a flexible name/value format that doesn’t require customisations to the RDBMS schema).

    I tried to say the same thing the other day but unfortunately in a more pretentious, obscure, abstract way: http://smellegantcode.wordpress.com/2010/02/11/multidimensional-separation-of-concerns/

    So how much of the stuff in your “core platform” could actually be described in this way? Could some of your core view/models be generated entirely of extensions? Maybe over time you could increase the expressiveness of the extension declaration system so more and more of your custom code can be swallowed up into “mere” extensions. Eventually for many views/models they would end up totally blank, all their content coming from extensions. How about multiple extensions contributing to a view in some controllable way?

    The advantage of this is that the core platform itself becomes a rich library of examples for anyone who wants to extend it. And it makes it practical to think not just about adding custom extensions here and there but also hiding parts, or arbitrarily re-arranging the site.

  • http://www.pacquiaovsclottey.info blogger

    What is the importance of blog using aspx under by asp.net?

  • http://craniometrics.blogspot.com J Healy

    I agree with Khalid that there are often scenarios requiring seperate persistence of the extension properties and would be interested to hear your thoughts on that.

    I would also be interested in hearing more about the view generation.

    This all might make for a good alt.Net Seattle session…

  • David Fauber

    Thanks for sharing this stuff, the parts I immediately grasp are really cool although some of it I’m going to need to actually sit down and hack through to really absorb it. I think I’m still having a hard time fully embracing convention over config as it feels like it would be somewhat anti-discoverable for someone that just got dropped in the middle of the codebase, but I’m working on it.

  • Khalid Abuhakmeh

    I liked the whole idea until you got to the database part. You were concerned about effecting your code base, but you were fine with modifying your existing database tables. I feel a better approach would be to “quarantine” the extension properties in a brand new table and let NHibernate join the two.

    You could take your extensions one step further and have the assembly migrate changes to and from the database. I don’t know how a DBA might feel about that, but it really gives more power to the developer.

    I am playing devil’s advocate now, How would a client add an extended property that is a list of things?

    Overall, really good post and definitely starred in Chrome.

  • http://craniometrics.blogspot.com J Healy

    An interesting approach to a problem Ayende touched on in his “Multi Tenancy – Extensible Data Model” post back in August 2008 :
    http://ayende.com/Blog/archive/2008/08/07/Multi-Tenancy–Extensible-Data-Model.aspx

    Would love to see a working code sample of it in action – would make a great MSDN article as well.

  • http://www.bjro.de Bjoern Rochel

    A+ post. Thank you for sharing this, Jeremy. Would love to here more about this

  • http://elegantcode.com Ryan Kelley

    Jeremy,

    Thank you for sharing this. You can bet I will be picking your brain a bit more on this. We are working on some stuff now that we were just talking about this on.

  • http://www.blogcoward.com jdn

    @Jeremy

    Yeah, I give you that. Still good stuff (TM) worth printing out to re-read.

    Please continue.

  • http://codebetter.com/members/jmiller/default.aspx Jeremy D. Miller

    @jdn,

    Doesn’t count. It’s just copy/paste code, not real content;)

  • http://www.blogcoward.com jdn

    I just printed this out to re-read later….11 pages.

  • Shane

    Jeremy, thanks for sharing this. I’ve approached this many different ways over the years and you’ve given me a great perspective on how we could do it better.