Skip to content
Michael Vivet edited this page Aug 29, 2024 · 96 revisions

Table of Contents


Summary

Models, also referred to as entities, represent the definitions of data in the application.

Derive a model implementation from DefaultEntity, and inherit the default probabilities of Nano models. Alternatively, derive models directly from the underlying interfaces and abstractions implemented by DefaultEntity, for greater flexibility and control over the model implementations.

Through the data mapping implementations, models are associated with a data context. Model represents tables and their properties columns, in the database.


Object Model

The object model, from which custom models can be derived, consists of a few abstractions and a set of interfaces.

The top-most interface definition for a model, is IEntity. Many parts of Nano requires a model to be of that type, so it should always be implemented by any model.

Most of the other interfaces defines the kind of operation is supported by the model. The repository pattern implemented by IRepository, expects models to implements the interfaces associated with the create, read, update and delete (CRUD) operations. See Services Operations for further details on operations used with models.

The interface IEntityIdentity<TIdentity>, defines an entity with an identifier property - Id. The TIdentity generic type parameter can be any desired type, though it's recommended to use System.Guid, which is also the default when deriving models from DefaultEntity. Note, that it's possible to specify the identifier when creating entities through controller create action.

Object Model Interfaces Diagram

Interfaces

The BaseEntity implements all interfaces, except IEntityIdentity<TIdentity>. The DefaultEntity derives from BaseEntity and additionally implements the interface IEntityIdentity<Guid>. Basically, deriving from the default implementation, enriches your models with a property for the unique identity and for the date it was created. Alternatively, use the DefaultEntity<TIdentity>, if the default implementation using Guid as identity isn't desired.

Object Model Base Classes Diagram

Classes


Data Mappings

Nano uses Entity Framework for mapping models to database tables.

The Data Context defines the class for managing the database session, and as explained associates models with their mappings, through the .OnModelCreating(...) method. The mapping implementations themselves, should derive from either BaseEntityMapping<TEntity> or DefaultEntityMapping<TEntity>, depending which base class the model is deriving from.

The base mappings implementations, maps the properties of which they are responsible, and additionally set a query filter for IsDeleted=0.

Table mapping
public class MyEntityMapping : DefaultEntityMapping<MyEntity>
{
    public override void Map(EntityTypeBuilder<MyEntity> builder)
    {
        base.Map(builder);

        // Entity Framework mapping.
    }
}
View mapping
public class MyEntityMapping : BaseEntityViewMapping<MyEntity>
{
    public override void Map(EntityTypeBuilder<MyEntity> builder)
    {
        base.Map(builder);

        // Entity Framework mapping.
    }
}

Query Criteria

Nano uses the DynamicExpression library to map properties of the query and criteria to a Linq Expression of the entity.

Query criteria defines a contract for a model. It's used when invoking the Query(...) method of the controller of the model. Adding properties to the contract and overridding the method GetExpression<TEntity>(), mapping the contract properties to the actual entity, will at runtime dynamically build the Expression<T> used in Linq queries by Entity Framework (or for that matter any Linq provider).

Obviously, since query criteria is only used by controller actions, there is no need for them when building console applications.

Query

The base query class contains pagination (number, count) and ordering (by, direction) properties. Naturally, these are used by controller actions and data queries to control the number of returned results as well as the order of which they are sorted.

Criteria

Nano contains a DefaultQueryCritiera class, implementing the IQueryCriteria interface, of the DynamicExpression library. It combines a Expression<TEntity> for the auditable properties IsActive, CreateAt and UpdatedAt, and expexts a DefaultEntity. If models doesn't derive from DefaultEntity, either derive the query criteria from BaseQueryCriteria or implement the interface IQueryCriteria directly.

So simply, create a class deriving from DefaultQueryCritiera, add criteria properties, and last override the GetExpression<TEntity>() method mapping the criteria properties to the entity properties, as shown below.

public class MyQueryCriteria : DefaultQueryCriteria
{
    public virtual string PropertyOne { get; set; }

    public override CriteriaExpression GetExpression<TEntity>()
    {
        var filter = base.GetExpression<TEntity>();

        if (this.PropertyOne != null)
            filter.StartsWith("PropertyOne", this.PropertyOne);

        return filter;
    }
}

Include Annotations

Annotating a property with the IncludeAttribte, instructs the repository layer to fetch additional data when getting and querying the entity. It works similar to the IQueryable.Include(...) extension, but allows for design-time definition. The property must be a class and have a navigation relations, otherwise the annotation is ignored.

The maximum query (join) depth for a model having properties decorated with IncludeAttribte, can be set in the data options of the configuration, QueryIncludeDepth.

When having navigations inside owned models decorated with include annotation, then the owned model property on the parent must also be annotated with Include. This will be ignored by entity framework, but allows Nano to trigger the nested include.

Note, that when creating or updating entities the include is not interpreted.


UX Exception Annotations

Annotating a model with the UxExceptionAttribte, instructs the exception handling middleware to return a custom translated error response, when a database unique index exception occurs, that matching the defined properties.
The attribute constructor takes two parameters. First, the custom error message to use when the Unique index exception is thrown. Second, an array of ordered properties, that aggregated should match the columns in the unique index.

It can make it easier to catch duplicate database entry exceptions.

Sample Implementation
[UxException("Duplicate name", nameof(Name)]
public class MyEntity : DefaultEntity
{
    public virtual string Name { get; set; }
}

Eventing Annotations

When building micro-service applications, managing relations and dependencies of shared models, becomes a challenge. The eventing attributes provides a very simple method publishing change notifications in one application, and subscribing to it in another.

Eventing attributes is similar to database foreign-key relations, just in between services. The PublishAttribute publishes an EntityEvent whenever an instance is either created, updated or deleted. When receiving an event subscribed to by a model annotated with the SubscribeAttribute, the built-in EntityEventHandler handles the event, and likewise creates, updates or deletes the instance.

The contract between the publisher and the subscriber is the type EntityEvent, using the entity type name (Type.Name, and not Type.FullName), when routing the event. The subscriber doesn't have any knowledge about the event being fired, and shouldn't. By convention, the relationship is loosely coupled.

Sample Implementation
[Publish(params)]
[Subscribe]
public class MyEntity : DefaultEntity
{
    [Phone]
    public virtual string PhoneNumber { get; set; }
}

If the Publish is used on a base class, that entity type will be used when publishing the entity event. This allows discriminated types in the master service to be published as the base to subscribing services. Also, it's possible through the params parameter of the Publish annotation, to pass additional properties in the event. Also nested classes are supported by using the . operator. Property names can be split on derived and base class Subscribe annotation, and when publishing the derived class, the property names of both itself and the base class will be concatenated. Be aware that changes to properties on related entities, won't trigger a publish. In this case you need to trigger the update of the entity after the related entity has been saved.

Publish/Subscribe can also work bi-directionally, but it would required the models in each service to have the same required properties, and probably best to keep them identical in this case.

NOTE: Entity events are sent through the repository implementation and not the database context. Bypassing the repository and using the database context directly, will also bypass the entity events.

NOTE: Avoid sharing models having SubscribeAttribute, with other Nano services, as the eventing subscription will be initialized unintentionally for that service as well.


Clone this wiki locally