# ModelBuilders - Advanced

The documentation on ModelBuilders has, thus far, concentrated on the different ways in which they can be utilized in code. What has not been touched upon as yet is their support for Domain Specific Language (DSL) within individual projects.

Whilst it is neither complicated or difficult to implement their use with DSL, it does require a basic understanding of C# generics and concepts such as type inference and generic constraints.

# Custom attributes

ModelBuilders were designed to be extensible and as a consequence they only deal with the following primitives:

  • IModelBuilder<TClassType> : IModelBuilder
  • IPropertyBuilder<TPropertyType, TClassType> : IPropertyBuilder
  • System.Attribute

The benefit confered by this is that solutions and individual projects are no longer tied to the specific attributes supported by XAF. ModelBuilders have the ability to support custom attributes.

By way of an example the classes in the code below illustrate a custom attribute ( ExportFormatAttibute ) and how it can be referenced in a ModelBuilder.































 

 
 







using System;
using DevExpress.ExpressApp.DC;
using Xenial.Framework.ModelBuilder;

namespace MainDemo.Module.BusinessObjects
{
    [AttributeUsage(
        AttributeTargets.Class | AttributeTargets.Property, 
        AllowMultiple = false, 
        Inherited = false
    )]
    public sealed class ExportFormatAttribute : Attribute
    {
        public string Format { get; }

        public ExportFormatAttribute(string format)
        {
            Format = format;
        }

        public ExportFormatAttribute()
        {
            Format = "My Default Format";
        }
    }

    public class DemoTaskModelBuilder : ModelBuilder<DemoTask>
    {
        public DemoTaskModelBuilder(ITypeInfo typeInfo) : base(typeInfo) { }

        public override void Build()
        {
            base.Build();

            this.WithAttribute(new ExportFormatAttribute("My Format"));

            For(m => m.Contacts)
                .WithAttribute<ExportFormatAttribute>();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

In making use of the WithAttribute() method it has been possible to apply the custom attribute to the business object.

TIP

There is another overload that accepts an action to configure the attribute.

.WithAttribute<ExportFormatAttribute>(a => a.Format = "My Format");
1

WARNING

Given that the majority of attributes are written to be immutable this convenience method may not always function as expected.

# Domain-extensions

Most developers will at some point encounter the difficulties that can occur when using domain types such as currency or dates. Whilst consistency is the ultimate goal it can be hard to achieve when a decimal type can describe money, percentages, pressure or any other unit and for example, the United States' use of a date format that is unrecognizable to the vast majority.

Within XAF itself it is not easy to solve these issues however the Xenial.Framework allows the creation of extension methods that can provide consistency in these situations and help to keep code DRY (opens new window).

The code below illustrates the application of a domain extension for the custom attribute ExportFormatAttribute by adding an extension method for the IModelBuilder<TClassType> and IPropertyBuilder<TPropertyType, TClassType> interfaces.

using MailClient.Module.BusinessObjects;

namespace Xenial.Framework.ModelBuilders
{
    public static partial class ModelBuilderDomainExtension
    {
        public static IModelBuilder<TClassType> HasExportFormat<TClassType>(
            this IModelBuilder<TClassType> modelBuilder, 
            string exportFormat = null
        )
        {
            if (exportFormat == null)
            {
                return modelBuilder.WithAttribute<ExportFormatAttribute>();
            }

            return modelBuilder.WithAttribute(new ExportFormatAttribute(exportFormat));
        }

        public static IPropertyBuilder<TPropertyType, TClassType> HasExportFormat<TPropertyType, TClassType>(
            this IPropertyBuilder<TPropertyType, TClassType> propertyBuilder, 
            string exportFormat = null
        )
        {
            if (exportFormat == null)
            {
                return propertyBuilder.WithAttribute<ExportFormatAttribute>();
            }

            return propertyBuilder.WithAttribute(new ExportFormatAttribute(exportFormat));
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

TIP

To make full use of IntelliSense and have it see these domain extensions automatically use the Xenial.Framework.ModelBuilders namespace. If using multiple domain extensions, classes can be marked as partial and reused across different files and projects.

WARNING

Avoid using the IModelBuilder and IPropertyBuilder interfaces directly as that may prevent these extension methods from remaining type safe. See the type safe section for more information.

# Combining attributes

Xenial.Framework is not restricted to the application of single custom attributes it is also possible to apply multiple attributes at once, illustrated in the code below.

namespace Xenial.Framework.ModelBuilders
{
    public static partial class ModelBuilderDomainExtension
    {
        public static IPropertyBuilder<TPropertyType, TClassType> AsNumeric<TPropertyType, TClassType>(
            this IPropertyBuilder<TPropertyType, TClassType> propertyBuilder
        )
        {
            return propertyBuilder
                .HasExportFormat("n0")
                .HasDisplayFormat("{0:n0}")
                .HasEditMask("n0");
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

The code above demonstrates succinctly that the addition of multiple custom attributes isn't difficult but at the same time masks a distinct issue. This domain extension only makes sense for properties of type int. The next section examines how it is possible to circumvent this.

TIP

We use that internally for example to combine the visibility properties like NotVisibleInAnyView().
See Built-in Attributes for more information.

# Type safe

ModelBuilders use the power of C# generics to provide a fluent API to specify metadata for business objects. By using type inference and generic constraints it is possible to limit what methods can be called on them.

As with previous examples involving ModelBuilders these concepts can be applied to both Classes and Properties.

# Class restriction

In the code below there is a business object that is not unlike the auditable light (opens new window) concept illustrated in the Dev Express knowledge base.












 


















 





 





 





 






 
 
 
 
 
 

using DevExpress.ExpressApp;  
using DevExpress.ExpressApp.Model;  
using DevExpress.Persistent.Base;  
using DevExpress.Persistent.BaseImpl;  
using DevExpress.Persistent.BaseImpl.PermissionPolicy;  
using DevExpress.Xpo;  
using System;  
using System.ComponentModel;  

namespace YourSolutionName.Module.BusinessObjects {  
    [DefaultClassOptions]  
    public class Contact : BaseObject, IAuditableLight {  
        public Contact(Session session)  
            : base(session) {  
        }  
        PermissionPolicyUser GetCurrentUser() {  
            //return Session.GetObjectByKey<PermissionPolicyUser>(SecuritySystem.CurrentUserId);  // In XAF apps for versions older than v20.1.7.
            return Session.FindObject<PermissionPolicyUser>(CriteriaOperator.Parse("Oid=CurrentUserId()"));  // In non-XAF apps where SecuritySystem.Instance is unavailable (v20.1.7+).
        }  
        public override void AfterConstruction() {  
            base.AfterConstruction();  
            CreatedOn = DateTime.Now;  
            CreatedBy = GetCurrentUser();  
        }  
        protected override void OnSaving() {  
            base.OnSaving();  
            UpdatedOn = DateTime.Now;  
            UpdatedBy = GetCurrentUser();  
        }  
        PermissionPolicyUser createdBy;  
        [ModelDefault("AllowEdit", "False")]  
        public PermissionPolicyUser CreatedBy {  
            get { return createdBy; }  
            set { SetPropertyValue("CreatedBy", ref createdBy, value); }  
        }  
        DateTime createdOn;  
        [ModelDefault("AllowEdit", "False"), ModelDefault("DisplayFormat", "G")]  
        public DateTime CreatedOn {  
            get { return createdOn; }  
            set { SetPropertyValue("CreatedOn", ref createdOn, value); }  
        }  
        PermissionPolicyUser updatedBy;  
        [ModelDefault("AllowEdit", "False")]  
        public PermissionPolicyUser UpdatedBy {  
            get { return updatedBy; }  
            set { SetPropertyValue("UpdatedBy", ref updatedBy, value); }  
        }  
        DateTime updatedOn;  
        [ModelDefault("AllowEdit", "False"), ModelDefault("DisplayFormat", "G")]  
        public DateTime UpdatedOn {  
            get { return updatedOn; }  
            set { SetPropertyValue("UpdatedOn", ref updatedOn, value); }  
        }  
    }  

    public interface IAuditableLight {
        PermissionPolicyUser CreatedBy { get; set; }
        DateTime CreatedOn { get; set; }
        PermissionPolicyUser UpdatedBy { get; set; }
        DateTime UpdatedOn { get; set; }
    }
}  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

Using the power of generics it becomes possible to apply attributes to just those classes that implement the IAuditableLight interface.








 





 



 
 



 



 
 






using YourSolutionName.Module.BusinessObjects;

namespace Xenial.Framework.ModelBuilders
{
    public static partial class ModelBuilderDomainExtension
    {
        public static IModelBuilder<TClassType> AsAuditableLight<TClassType>(this IModelBuilder<TClassType> modelBuilder)
            where TClassType : class, IAuditableLight
        {
            const string displayFormat = "{0:G}";

            modelBuilder
                .For(m => m.CreatedBy)
                .NotAllowingEdit();

            modelBuilder
                .For(m => m.CreatedOn)
                .NotAllowingEdit()
                .HasDisplayFormat(displayFormat);

            modelBuilder
                .For(m => m.UpdatedBy)
                .NotAllowingEdit();

            modelBuilder
                .For(m => m.UpdatedOn)
                .NotAllowingEdit()
                .HasDisplayFormat(displayFormat);

            return modelBuilder;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

TIP

The code above is a very simple example for which a custom base object could have been used but that would be missing the point that frequently that wouldn't be possible because XPO does not allow the use of associations in non persistent classes.

With this approach metadata can be kept consistent across multiple classes.

# Property restriction

The example used above for classes had a display format for DateTime properties. In the one below an extension will be created for that.

using System;

namespace Xenial.Framework.ModelBuilders
{
    public static partial class ModelBuilderDomainExtension
    {
        private static const string LongDateGeneralFormat = "{0:G}";

        public static IPropertyBuilder<DateTime, TClassType> HasLongGeneralDateFormat<TClassType>(this IPropertyBuilder<DateTime, TClassType> propertyBuilder)
        {
            return propertyBuilder.HasDisplayFormat(LongDateGeneralFormat);
        }

        public static IPropertyBuilder<DateTime?, TClassType> HasLongGeneralDateFormat<TClassType>(this IPropertyBuilder<DateTime?, TClassType> propertyBuilder)
        {
            return propertyBuilder.HasDisplayFormat(LongDateGeneralFormat);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

The example above doesn't use a contraint, rather it uses the type itself. There is a sound logical reason for this. DateTime is a struct and and as such can't be inherited. By adopting this approach the compiler and IntelliSense will only show the extension methods for DateTime properties.

TIP

Because of a restriction in the C# compiler Xenial.Framework provides two date overloads, one for nullable dates and the other for non nullable dates.

Whilst it may seem onerous at first to have to differentiate between the two it will soon become apparent that it confers benefits that wouldn't be possible with just a single overload for all dates.

WARNING

If the extension does not show up in IntelliSense or the compiler reports errors, it might be because the generic parameters have been transposed.

//WRONG:
IPropertyBuilder<TClassType, DateTime>
//CORRECT:
IPropertyBuilder<DateTime, TClassType>
1
2
3
4

# Advanced builders

For more advanced builders such as the convention builder it is possible to work with the primitive IBuilder.

Whilst an example of that is not provided within this documentation there are examples of it's use in the source code of the framework and the tests that accompany it. If problems are encountered whilst using these please raise them in the issues (opens new window).

using System;
using System.Linq;

namespace Xenial.Framework.ModelBuilders;

public partial class ModelBuilder<TClassType>
{
    /// <summary>   Fors all properties. </summary>
    ///
    /// <returns>
    /// IAggregatedPropertyBuilder&lt;System.Nullable&lt;System.Object&gt;, TClassType&gt;.
    /// </returns>

    public IAggregatedPropertyBuilder<object?, TClassType> ForAllProperties()
    {
        var propertyBuilders = TypeInfo.Members.Select(m => PropertyBuilder.PropertyBuilderFor<object?, TClassType>(m));

        foreach (var propertyBuilder in propertyBuilders)
        {
            Add(propertyBuilder);
        }

        return new AggregatedPropertyBuilder<object?, TClassType>(this, propertyBuilders);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using System;
using System.Collections.Generic;

using DevExpress.ExpressApp.DC;

namespace Xenial.Framework.ModelBuilders;

/// <summary>
/// Class AggregatedPropertyBuilder.
/// Implements the <see cref="Xenial.Framework.ModelBuilders.IPropertyBuilder{TPropertyType, TClassType}" />
/// </summary>
/// <typeparam name="TPropertyType">The type of the t property type.</typeparam>
/// <typeparam name="TClassType">The type of the t class type.</typeparam>
/// <seealso cref="Xenial.Framework.ModelBuilders.IPropertyBuilder{TPropertyType, TClassType}" />
/// <autogeneratedoc />
[XenialCheckLicense]
public sealed partial class AggregatedPropertyBuilder<TPropertyType, TClassType> : IAggregatedPropertyBuilder<TPropertyType, TClassType>
{
    private readonly IEnumerable<IPropertyBuilder<TPropertyType, TClassType>> propertyBuilders;
    private readonly IModelBuilder<TClassType> modelBuilder;

    IEnumerable<IPropertyBuilder<TPropertyType, TClassType>> IAggregatedPropertyBuilder<TPropertyType, TClassType>.PropertyBuilders
        => propertyBuilders;

    IModelBuilder<TClassType> IAggregatedPropertyBuilder<TPropertyType, TClassType>.ModelBuilder
        => modelBuilder;

    /// <summary>
    /// Initializes a new instance of the
    /// <see cref="AggregatedPropertyBuilder{TPropertyType, TClassType}" /> class.
    /// </summary>
    ///
    /// <exception cref="ArgumentNullException">    propertyBuilders. </exception>
    ///
    /// <param name="modelBuilder">     The model builder. </param>
    /// <param name="propertyBuilders"> The property builders. </param>

    public AggregatedPropertyBuilder(IModelBuilder<TClassType> modelBuilder, IEnumerable<IPropertyBuilder<TPropertyType, TClassType>> propertyBuilders)
    {
        this.modelBuilder = modelBuilder;
        this.propertyBuilders = propertyBuilders ?? throw new ArgumentNullException(nameof(propertyBuilders));
    }

    string IPropertyBuilder.PropertyName => throw new NotImplementedException();
    IMemberInfo IPropertyBuilder.MemberInfo => throw new NotImplementedException();

    /// <summary>   Removes the attribute. </summary>
    ///
    /// <typeparam name="TAttribute">   The type of the attribute. </typeparam>
    ///
    /// <returns>   IPropertyBuilder&lt;TPropertyType, TClassType&gt;. </returns>

    public IPropertyBuilder<TPropertyType, TClassType> RemoveAttribute<TAttribute>()
        where TAttribute : Attribute
    {
        foreach (var propertyBuilder in propertyBuilders)
        {
            propertyBuilder.RemoveAttribute<TAttribute>();
        }
        return this;
    }

    /// <summary>   Removes the attribute. </summary>
    ///
    /// <param name="attribute">    The attribute. </param>
    ///
    /// <returns>   IPropertyBuilder&lt;TPropertyType, TClassType&gt;. </returns>

    public IPropertyBuilder<TPropertyType, TClassType> RemoveAttribute(Attribute attribute)
    {
        foreach (var propertyBuilder in propertyBuilders)
        {
            propertyBuilder.RemoveAttribute(attribute);
        }
        return this;
    }

    /// <summary>   Withes the attribute. </summary>
    ///
    /// <typeparam name="TAttribute">   The type of the attribute. </typeparam>
    ///
    /// <returns>   IPropertyBuilder&lt;TPropertyType, TClassType&gt;. </returns>

    public IPropertyBuilder<TPropertyType, TClassType> WithAttribute<TAttribute>()
        where TAttribute : Attribute, new()
    {
        foreach (var propertyBuilder in propertyBuilders)
        {
            propertyBuilder.WithAttribute<TAttribute>();
        }
        return this;
    }

    /// <summary>   Withes the attribute. </summary>
    ///
    /// <typeparam name="TAttribute">   The type of the attribute. </typeparam>
    /// <param name="attribute">    The attribute. </param>
    ///
    /// <returns>   IPropertyBuilder&lt;TPropertyType, TClassType&gt;. </returns>

    public IPropertyBuilder<TPropertyType, TClassType> WithAttribute<TAttribute>(TAttribute attribute)
        where TAttribute : Attribute
    {
        foreach (var propertyBuilder in propertyBuilders)
        {
            propertyBuilder.WithAttribute(attribute);
        }
        return this;
    }

    /// <summary>   Withes the attribute. </summary>
    ///
    /// <typeparam name="TAttribute">   The type of the attribute. </typeparam>
    /// <param name="configureAction">  The configure action. </param>
    ///
    /// <returns>   IPropertyBuilder&lt;TPropertyType, TClassType&gt;. </returns>

    public IPropertyBuilder<TPropertyType, TClassType> WithAttribute<TAttribute>(Action<TAttribute> configureAction)
        where TAttribute : Attribute, new()
    {
        foreach (var propertyBuilder in propertyBuilders)
        {
            propertyBuilder.WithAttribute(configureAction);
        }
        return this;
    }

    /// <summary>   Withes the attribute. </summary>
    ///
    /// <typeparam name="TAttribute">   The type of the attribute. </typeparam>
    /// <param name="attribute">        The attribute. </param>
    /// <param name="configureAction">  (Optional) The configure action. </param>
    ///
    /// <returns>   IPropertyBuilder&lt;TPropertyType, TClassType&gt;. </returns>

    public IPropertyBuilder<TPropertyType, TClassType> WithAttribute<TAttribute>(TAttribute attribute, Action<TAttribute>? configureAction = null)
        where TAttribute : Attribute
    {
        foreach (var propertyBuilder in propertyBuilders)
        {
            propertyBuilder.WithAttribute(attribute, configureAction);
        }
        return this;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145