Skip to content
Nick Airdo edited this page Jul 7, 2015 · 11 revisions

TODO Write a brief explanation of entities or the Entity Framework.

All Rock models should extend Model unless they don't require security and attributes (which would be rare). In that case, they extend Entity.

Code First

TODO Explain how a 3rd party developer creates their own custom entities.

NOTE: If you're new to the Entity Framework and you want to learn more about what's going on behind the scenes in Rock, you should read the Get Started with Entity Framework and/or the Working With DbContext MSDN articles.

First, create simple POCO classes which represent your entities and add them to the Model folder of the Rock project. Decorate each entity property with a [DataMember] attribute as needed. NOTE: Refer to the Naming-Conventions document for standard field and property naming when creating new classes.

If your entity has a relationship to another entity or an entity collection, remember to add a virtual property to achieve EF Lazy Loading. For example, as seen in the Person entity, a person can have multiple phone numbers, so the PhoneNumbers virtual property looks like this:

    public partial class Person : Model<Person>
    {
        // ...
        [DataMember]
        public virtual ICollection<PhoneNumber> PhoneNumbers { get; set; }

After creating the necessary POCO classes, run the Rock.CodeGeneration.exe tool (found in the Rock.CodeGeneration project) to auto-generate the necessary corresponding services, controllers and REST classes for your entities. Once they have been auto-generated, include them to the main Rock and Rock.Rest projects. Add any of your own custom code into a corresponding *.Partial.cs code file in the either the Rock project's Model folder (next to the POCO class) or the Rock.Rest project's Controller folder.

NOTE: For an example of a partial class which adds additional custom functionality in the Rock.Rest project, see the BlocksController in the BlocksController.Partial.cs file.

Next, add any new DbSet<type> properties into the Models region in the Rock\Data\RockContext.cs as illustrated with this example of the Campus entity:

    public DbSet<Campus> Campuses { get; set; }

Lastly, add any new entity configurations into the ContextHelper.AddConfigurations() method of the ContextHelper like this:

    modelBuilder.Configurations.Add( new CampusConfiguration() );

Class Decorator Attributes

As you've already seen above, Rock uses certain standard and custom .NET attributes to infer things about classes and/or their properties. Besides the standard Code First data annotation attributes used by EF and Rock validation (see Validation section below) the following is a list of custom attributes and what effect they have in Rock. You can read more about the new EF6 Custom Code Conventions functionality.

NOTE: Rock also use decorator attributes to denote BlockType Field Attributes. See the "Block Attributes" section in the for details.

[AlternateKey]

Any property that will be frequently used as a searches (LINQ queries) should be decorated with [AlternateKey]. It will be used by the migration code generator to create an index for that entity's table column.

[DatabaseGenerated]

If a property is being computed in the database, then you don't want Entity Framework to try to update those columns. In these cases, use the DatabaseGenerated annotation to flag those properties in your class along with the Computed enum as seen in this example:

  [DataMember]
  [DatabaseGenerated( DatabaseGeneratedOption.Computed )]
  [Previewable]
  [MergeField]
  public string FirstName
  {
      get
      {
          return string.IsNullOrEmpty( NickName ) ? GivenName : NickName;
      }
  }

[DataMember]

This is a critical EF attribute, but among other things, this decorator will also control whether or not the JsonConvert.SerializeObject function will serialize the property.

[DefinedValue]

A property that holds the value of a particular DefinedType's DefinedValue should be decorated with the [DefinedValue( guid definedTypeGuid )] as seen in this example from the FinancialPledge class:

   [DataMember]
   [DefinedValue( SystemGuid.DefinedType.FINANCIAL_FREQUENCY )]
   public int? PledgeFrequencyValueId { get; set; }

TODO Explain the reasons. I know it has something to do with DataViews but I'm not sure what. I did find something in the ReportDetail block but I'm not certain.

[MergeField]

TODO Explain what and why.

[NotAudited]

If decorated on the entire class, no object instance changes will be recorded to the Audit table (entity). If decorated on a property, then changes to that property will not be recorded when other changes are detected on instances of an object.

Example 1 - The entire Audit entity class is decorated to avoid creating duplicate audits.

    [NotAudited]
    public partial class Audit : Entity<Audit>
    {

Example 2 - The UserLogin entity's LastActivityDate property is not audited in order to avoid excessive data collection.

    public partial class UserLogin : Model<UserLogin>
    {
        // ...

        [NotAudited]
        public DateTime? LastActivityDate { get; set; }

[Previewable]

Decorating a property of your Entity with [Previewable] will inform Rock that it should be shown in column/tabular when viewing preview results of the entity such as those seen in the Reporting DataView blocks.

    public partial class Person : Model<Person>
    {
        // ...
        [Previewable]
        public string GivenName { get; set; }

        [Previewable]
        public string LastName { get; set; }

A A "preview" of a DataView showing properties that have the [Previewable] attribute

Security

Rock supports a flexible security system that can be used for both object level security and/or entity-type based security. The framework's security can be thought to work like this:

  1. First, authorization is checked for the particular entity instance.
  2. If no authorization rules/records were found, then the entity's ParentAuthority property is used to check for authorization.

Let's consider the 'page security' example to help illustrate this mechanism. When determining whether or not a person has access to view a page, the page's security is checked, followed by it's parent page (which is a page's 'ParentAuthority), etc. up to the root page. However if a page does not have a parent page (ie, a root-level page) the Pageentity class defines theParentAuthorityas the page'sLayout.Site`.

ParentAuthority

By overriding the ParentAuthority in your Entity class, you can control what will be used when the framework checks for authorization (if no authorization exists for the object/instance). For example, looking at Rock's Model\Group.cs class we can see that a Group's ParentAuthority is first its parent group (if one exists) otherwise it falls back use its GroupType:

    public override Security.ISecured ParentAuthority
    {
        get
        {
            if ( this.ParentGroup != null )
            {
                return this.ParentGroup;
            }
            else
            {
                return this.GroupType;
            }
        }
    }

Similarly, as mentioned above, the Page class defines the ParentAuthority as the page's Layout.Site if it has no parent page:

    public override Security.ISecured ParentAuthority
    {
        get
        {
            return ( this.ParentPage != null )
            {
                return this.ParentPage;
            }
            else
            {
                return this.Layout.Site;
            }
        }
    }

Validation

Validation starts with a properly coded Entity. Since Rock is using Entity Framework's automatic validation features, all you really need to worry about is decorating your entity's properties with the right data annotation attributes:

Example:

    [Required]
    [MaxLength( 255 )]
    public string MimeType { get; set; }

Using these annotations would make your entity to require a value on the MimeType property and would not allow more than 255 characters in the value.

Here a few of the more common annotations you should be familiar with, but a complete list of data annotation attribute classes can be seen on the MSDN site.

[Required] - Indicates that a value is required.

[MaxLength(int)] - Specifies the maximum length of array or string data allowed.

[Range(int, int)] - Specifies the numeric range constraints for the value.

[Url] - Provides URL validation.

[RegularExpression(string)] - Specifies that the value must match the specified regular expression pattern.

Example:

        [RegularExpression(@"^[a-zA-Z0-9]+$", ErrorMessage = "Please enter only alpha-numeric characters.")]

[StringLength(max)] - Specifies the minimum and maximum length of characters that are allowed.

Example:

    [StringLength( 250, MinimumLength = 3 )]

Tip: Be sure to check out the [validation section on the topic of Blocks][link1] [link1]: Blocks#Validation

Audit Logging

All changes to entities will now be saved to the Audit table. The [NotAudited] attribute can be added to a class or property to prevent changes to that class or property from being logged to the audit table. (Currently the ExceptionLog, EntityChange, and Audit entities are the only models not logging changes. Any changes to the LastActivityDate property on the UserLogin table are not logged but the LastLoginDate is logged.) When a property on an entity has the [NotAudited] attribute changes to that entity will only get logged if at least one of the other properties (without the attribute) are modified.

The Audit table includes the entity type, it's name (title), the properties changed and the type of update (Add, Modify, Delete), the date and ID of the person making the change.

Helper Methods

There are several useful classes/methods in the Rock project you may want to become familiar with.

Rock.Reflection

  • FindTypes() – Static method that will return a sorted dictionary object of all types that inherit from a specified base type. Will search through the Rock.dll and any other dll in the same folder (web/bin) that have a pattern of Rock.*.dll
  • ClassName() – Static method that will return the [Description] attribute value of a given class type, or the class name if the attribute does not exist.

Tips and Tricks

LINQ Performance Tips

  • .AsNoTracking()

For example

// Change this:
foreach ( var groupMember in service.GetByGroupId( family.Group.Id ).ToList() )

// to this:
foreach ( var groupMember in service.GetByGroupId( family.Group.Id ).AsNoTracking().ToList() )

Avoiding EF Deferred Execution

This is something to be aware of when using the Entity Framework. There are certain scenarios where we would want to avoid getting data through EF's Deferred Execution. An example:

The GetByEntity() method below returns an IQueryable object. Each TaggedItem object has a parent "Tag" object that has the name and owner of the tag. In the before code, a SQL query would be run to get all the tagged items, and then additional SQL query would be run in each iteration of the foreach loop to get the parent Tag object as soon as the parent "Tag" property was referenced.

Since we know before-hand that we need the Tag.Name, and Tag.OwnerId, we can modify the IQueryable object to return an anonymous object with that info which results in the initial SQL query including that data, and thus only one query is run.

Before:

    foreach ( var item in service.GetByEntity( base.EntityType, entityQualifierColumn,
        entityQualifierValue, CurrentPersonId, base.Entity.Id ))
    {
        tagNames.Add( string.Format( "{0}{1}", item.Tag.Name,
            CurrentPersonId.HasValue && item.Tag.OwnerId == CurrentPersonId.Value ? "^personal" : "" ) );
    }

After:

    foreach ( dynamic item in service.GetByEntity( base.EntityType, entityQualifierColumn,
        entityQualifierValue, CurrentPersonId, base.Entity.Id )
            .Select( i => new {
                OwnerId = i.Tag.OwnerId,
                Name = i.Tag.Name
        }))
    {
        tagNames.Add( string.Format( "{0}{1}", item.Name,
            CurrentPersonId.HasValue && item.OwnerId == CurrentPersonId.Value ? "^personal" : "" ) );
    }
Clone this wiki locally