Entities File Settings

Settings for scaffolding out an entity for your Web API.

Entities

A list of database entities in your project. These entities are added as dbsets to your database context, so they translate 1:1 to tables in your database. With that said, if you want to create an aggregate, you can scaffold out an entity and update it accordingly to reflect that aggregate yourself (e.g. remove the db set).

Entity Properties

NameRequiredDescriptionDefault
NameYesThe name of the entityNone
PropertiesYesA list of properties assigned to your entity described in the entity properties section.None
FeaturesYesThis is a list of features that you want to add for a particular entity.None
PluralNoThe plural of the entity name, if needed (e.g. Cities would prevent Citys)Entity Name appended with an s
LambdaNoThe value to use in lambda expressions for this entity.First letter of the entity name.
TableNameNo (Yes if you want to set a Schema)The name of the table in the database.Defaulted to the singular Name.
SchemaNoThe schema to use in the database.None, which will equate to dbo in SqlServer, public in Postgres, or mydb in MySQL

TableName is required if you want to set a schema

Properties

A list of properties that can be assigned to an entity.

NameRequiredDescriptionDefault
NameYesThe name of the propertyNone
TypeYesThe data type for the property. These are not case sensitive and they can be set to nullable with a trailing ?. All standard C# data types can be used here. You even do custom properties like classes that act as foreign keys for instance.string
CanFilterNoWill set the property to be filterable in the API endpoint when set to true.false
CanSortNoWill set the property to be filterable in the API endpoint when set to true.false
IsRequiredNoWhen true, the property will be set as required in the database.false
true for primary key
ColumnNameNoThe database field name for the given property.None
CanManipulateNoWhen set to false, you will not be able to update this property when calling the associated endpoint. When set to false, the property will be able to be established when using the POST endpoint, but will not be able to be updated after that. This is managed by the DTOs if you want to manually adjust this.true
false for primary key
DefaultValueNoAllows you to add a default value to a property. Note that it should be entered exactly as you'd want it, so a string would be something like "My Default Value", a bool might be true, and an int might be 0None
ForeignEntityNameNoCaptures the name of the entity this property is linked to as a foreign key.None
ForeignEntityPluralNoThe plural value for the foreign entity, if applicable.Entity Name appended with an s
SmartNamesNoA list of strings used to create a smart enum property. Each string denotes an option for the smart enum. See the smart enum example below for details. Property attributes like filtering and sorting can be used if desired.None

Features

NameRequiredDescriptionDefault
TypeYesThe type of feature you want to add. The accepted values are AdHoc, GetRecord, GetList, DeleteRecord, UpdateRecord, PatchRecord, AddRecord, and CreateRecord (same as AddRecord but available as an alias in case you can't remember!)None
IsProtectedNoDetermines whether or not the feature is protected with an authorization policy attribute.false
PermissionNameNoThe name of the authorization policy attribute.Can[FEATURENAME]

GetRecord, GetList, DeleteRecord, UpdateRecord, PatchRecord, AddRecord, and AddListByFk are all standard CRUD features along with their endpoints and associated tests.

ℹ DTOs, validators, profiles and functional test routes associated to all features will be added regardless of whether or not a given feature was added. This is due to the complexity of all the minute management around handling these so dynamically. Feel free to delete whatever you're not using.

AddListByFk Feature

The AddListByFk will let you add a list or batch of records using a foreign key. For example, you can add a list of ingredients for a recipe, based on the id of a recipe.

NameRequiredDescriptionDefault
BatchPropertyNameYesThe name of the property on the foreign entity you are doing a batch add on.None
BatchPropertyTypeYesThe data type of the the property you are doing the batch add on. This is for a FK property, so probably a Guid or int.None
ParentEntityYesThe name of the parent entity that the FK you using is associated to. For example, if you had a FK of EventId, the parent entity might be Event. Leave null if you're not batching on a FK.None
ParentEntityPluralYesThe plural of the FK entity. Leave null if you're not batching on a FK.None

Ad Hoc Features

You can also add ad hoc skeletons with the AdHoc type. For AdHoc features, there's a bit more that needs to be done. In addition to Type above, you'll want to provide the below:

NameRequiredDescriptionDefault
NameYesThe name of the feature. This is the name of the feature's method and would generally be something like AddCustomer or GetCustomer.None
CommandYesThe name of the command. This is the name of the feature's class and would generally be something like AddCustomerCommand or GetCustomerQuery.[NAME]Command
EntityPluralNoThe plural name of the entity that the feature will be added to (should match the name in the Domain directory). You can leave this blank to add it to the Domain directory directly.[ENTITYNAME]s
ResponseTypeNoIs the type of response that this command will return. This could be any C# type that you want, or even a custom type if you want, but in that case, you'll need to add a using statement for it manually.bool

You can also add any of the above features after the fact using the add feature command.

Keys

There are a couple of important pieces of information to know about settings keys for scaffolding:

  1. As v0.12+, you do not need to add a primary key property to your entities as they will automatically be inherited from the BaseEntity.
  2. Foreign keys can be set as shown in the examples below, but note that, in many cases, these will break some of your tests. You will likely need to update your tests fakers and
  3. The 'Many' side of a foreign key will not be added to DTOs with scaffolding and will need to be added manually if you want to use it there. The 'Id' of a 'One' relationship will be added though.

Updating Tests That Use Foreign Keys to Green

This is the most noteworthy of the items above and will require the most manual intervention. Scaffolding here is tricky due to the variety of situations that can arise, so they sometimes need to be resolved manually. Here are a few tricks that may help:

  1. Update your fakers. Autofaker will, by default, try and scaffold out your child or parent objects which can break your tests. Depending on your situation, you may need to set the foreign key to a particular value. Here are a couple rules that you could add
// you could create an empty list so that your tests don't have to accommodate the default of 3. only add them if you need them for a test
RuleFor(s => s.Organizations, _ => new List<Organization>());

// if you have a single foreign key, you may want to set the object and/or the id to null
RuleFor(r => r.RecipeId, _ => null);
RuleFor(r => r.Recipe, _ => null);

// in some cases, your object may not make sense in your domain without having the object, so could generate a fake for it using your designated faker that has its own rules.
RuleFor(i => i.Recipe, _ => new FakeRecipe().Generate());
  1. Update the Arrange in your tests.

For example, this could mean inserting a parent object into the database that can be assigned to the child to prevent a foreign key conflict.

[Test]
public async Task can_add_new_ingredient_to_db()
{
    // Arrange
    var fakeRecipeOne = new FakeRecipe { }.Generate();
    await InsertAsync(fakeRecipeOne);

    var fakeIngredientOne = new FakeIngredientForCreationDto { }.Generate();
    fakeIngredientOne.RecipeId = fakeRecipeOne.Id;

    // Act
    var command = new AddIngredient.AddIngredientCommand(fakeIngredientOne);
    var ingredientReturned = await SendAsync(command);
    var ingredientCreated = await ExecuteDbContextAsync(db => db.Ingredients.SingleOrDefaultAsync());

    // Assert
    ingredientReturned.Should().BeEquivalentTo(fakeIngredientOne, options =>
        options.ExcludingMissingMembers());
    ingredientCreated.Should().BeEquivalentTo(fakeIngredientOne, options =>
        options.ExcludingMissingMembers());
}

Or making sure that you don't try and insert a user id that isn't in the db.

[Test]
public async Task can_update_existing_ingredient_in_db()
{
    // Arrange
    var fakeIngredientOne = new FakeIngredient { }.Generate();
    await InsertAsync(fakeIngredientOne);

    var updatedIngredientDto = new FakeIngredientForUpdateDto { }.Generate();
    updatedIngredientDto.UserId = fakeIngredientOne.User.Id;

    var ingredient = await ExecuteDbContextAsync(db => db.Ingredients.SingleOrDefaultAsync());
    var id = ingredient.Id;

    // Act
    var command = new UpdateIngredient.UpdateIngredientCommand(id, updatedIngredientDto);
    await SendAsync(command);
    var updatedIngredient = await ExecuteDbContextAsync(db => db.Ingredients.Where(u => u.Id == id).SingleOrDefaultAsync());

    // Assert
    updatedIngredient.Should().BeEquivalentTo(updatedIngredientDto, options =>
        options.ExcludingMissingMembers());
}

Entity Example

This example would create a supplier entity with several properties. Note:

  • The SupplierId is marked as a primary key
  • We have normal and nullable primitives in the Type option
  • We have marked the properties as sortable and filterable
  • We have given a default value for IsVip
  • We are adding all the available features except for PatchRecord. We're also adding an AdHoc feature for use for batch adding suppliers.
Entities:
  - Name: Supplier
    Features:
      - Type: AddRecord
      - Type: GetRecord
      - Type: GetList
      - Type: UpdateRecord
      - Type: DeleteRecord
      - Type: AddListByFk
        BatchPropertyName: OrganizationId
        BatchPropertyType: Guid
        ParentEntity: Organization
        ParentEntityPlural: Organizations
      - Type: AdHoc
        Name: MySpecialLookup
        Command: MySpecialLookupQuery
        EntityPlural: Suppliers
    Properties:
      - Name: Name
        Type: string
        CanFilter: true
        CanSort: true
      - Name: EmployeeCount
        Type: int?
        CanFilter: true
        CanSort: true
      - Name: CreationDate
        Type: datetime?
        CanFilter: true
        CanSort: true
      - Name: SupplierType
        Type: int?
        CanFilter: true
        CanSort: true
      - Name: IsVip
        Type: bool?
        CanFilter: true
        CanSort: true
        DefaultValue: false
      - Name: OrganizationId
        Type: Guid?
        ForeignEntityName: Organization

Example With Foreign Key

Foreign keys can be added in any of the below ways. At a high level, you'll notice there is a syntax for adding the 1 relationship type and the many relationship type.

One to One Relationships

Entities:
  - Name: User
    Properties:
      - Name: Name
        Type: string
      - Name: UserProfileId
        Type: Guid?
        ForeignEntityName: UserProfile
        ForeignEntityPlural: UserProfiles
  - Name: UserProfile
    Properties:
      - Name: Name
        Type: string
      - Name: UserId
        Type: Guid
        ForeignEntityName: User
        ForeignEntityPlural: Users

One to Many Relationships

Here, we have a Blog with many Posts. The Type of the foreign id doesn't have to be a Guid, but is in this example. Additionally, the many Posts can be any collection that works for a normal .NET relationship, in this example, an ICollection. It does also need a ForeignEntityPlural to allow for a proper using statement.

Entities:
  - Name: Post
    Properties:
      - Name: Name
        Type: string
      - Name: BlogId
        Type: Guid?
        ForeignEntityName: Blog
        ForeignEntityPlural: Blogs
  - Name: Blog
    Properties:
      - Name: Name
        Type: string
      - Name: Posts
        Type: ICollection<Post>
        ForeignEntityPlural: Posts

Many to Many Relationships

Here, we have many Books that can also have many Categories. Like the One to Many relationship, the Type can be any collection that works for a normal .NET relationship. As with one to many, they need a ForeignEntityPlural to allow for a proper using statement.

Entities:
  - Name: Book
    Properties:
      - Name: Name
        Type: string
      - Name: Categories
        Type: ICollection<Category>
        ForeignEntityPlural: Categories
  - Name: Category
    Properties:
      - Name: Name
        Type: string
      - Name: Books
        Type: ICollection<Book>
        ForeignEntityPlural: Books

Composite Keys

For the time being, composite keys can not be created using Craftsman commands. With that said, you are more than welcome to scaffold out an entity with one of the keys and then modify the generated entity, repository, tests, etc. to accommodate that composite key.

Policies

If using auth, you can protect features with a particular permission. In the example below, I have a permission for doing read overall and default permissions for each destructive feature.

Entities:
  - Name: Recipe
    Features:
      - Type: GetList
        IsProtected: true
        PermissionName: CanReadRecipes
      - Type: GetRecord
        IsProtected: true
        PermissionName: CanReadRecipes
      - Type: AddRecord
        IsProtected: true
      - Type: UpdateRecord
        IsProtected: true
      - Type: DeleteRecord
        IsProtected: true
      - Type: PatchRecord
        IsProtected: true
    Properties:
      - Name: Title
        Type: string
        CanFilter: true
        CanSort: true
      - Name: Directions
        Type: string
        CanFilter: true
        CanSort: true

Smart Enums

If you want to use smart enums, you can use the SmartNames property like so:

Entities:
  - Name: Recipe
    Features:
    #...
    Properties:
      - Name: Title
        Type: string
        CanFilter: true
        CanSort: true
      - Name: Visibility
        SmartNames:
          - Public
          - Friends Only
          - Private
        CanFilter: true
        CanSort: true