> For the complete documentation index, see [llms.txt](https://aptitude.gitbook.io/zaubercms/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://aptitude.gitbook.io/zaubercms/extending-zaubercms/before-and-after-save.md).

# Before & After Save

If you want to make changes to an entity (i.e. Content, Media etc...) before it's saved or after it's saved, or when something new is added or deleted. You can use the IBeforeEntitySave plugin.

Just implement the Interface

### Interfaces

```csharp
/// <summary>
/// Defines a contract for handling operations that need to occur before saving an entity to the database.
/// </summary>
public interface IBeforeEntitySave<TEntity>
{
    /// <summary>
    /// Executes operations needed before saving an entity to the database.
    /// </summary>
    /// <param name="entity">The entity instance that is about to be saved.</param>
    /// <param name="entityState">The state of the entity within the context (e.g., Added, Modified, Deleted).</param>
    /// <returns>Returns false if the save operation should be canceled; otherwise, true.</returns>
    bool BeforeSave(TEntity entity, EntityState entityState);

    /// <summary>
    /// Gets or sets the order in which operations or processes should be executed or entities should be managed.
    /// </summary>
    /// <remarks>
    /// This property is typically used to indicate the priority or position of an entity or operation
    /// in contexts such as sorting, processing, or UI display.
    /// </remarks>
    int SortOrder { get; }
}
```

```csharp
public interface IAfterEntitySave<TEntity>
{
    /// <summary>
    /// Executes operations needed after saving an entity to the database.
    /// </summary>
    /// <param name="entity">The entity instance that is about to be saved.</param>
    /// <param name="entityState">The state of the entity within the context (e.g., Added, Modified, Deleted).</param>
    /// <returns>True if the entity needs to be resaved</returns>
    bool AfterSave(TEntity entity, EntityState entityState);

    /// <summary>
    /// Gets or sets the order in which the operation should be executed after saving an entity.
    /// </summary>
    int SortOrder { get; }
}
```

### Usage

And then you can check the entity state or just just update the entity before it's saved. Very simple example below showing the save being abandoned

```csharp
public class StopSaveIfBadWord : IBeforeEntitySave<Content>
{
    public bool BeforeSave(Content entity, EntityState entityState)
    {
        if (entity.Name != null && entity.Name.Contains("Arsenal"))
        {
            // Horrible word so don't let them save
            return false;
        }
        return true;
    }
}
```

Or an example showing some content being replaced

```csharp
public class StopSaveIfBadWord : IBeforeEntitySave<Content>
{
    public bool BeforeSave(Content entity, EntityState entityState)
    {
        if (entity.Name != null && entity.Name.Contains("Arsenal"))
        {
            entity.Name = entity.Name.Replace("Arsenal", "Up The Spurs #COYS");
        }
        return true;
    }
}
```

### Advanced Example

In this example, I am creating a autogenerated blog snippet of 30 words, taken from a specific element type in a block list.  I then update the snippet on the content and use it for the meta description if that is blank (The meta description is part of the SEO property editor).

```csharp
public partial class AddBlogSnippetOnSave : IBeforeEntitySave<Content>
{
    public bool BeforeSave(Content entity, EntityState entityState)
    {
        if (entity.ContentTypeAlias == "BlogPage")
        {
            var sb = new StringBuilder();
            foreach (var block in entity.GetValue<List<Content>>("MainContent") ?? [])
            {
                if (block.ContentTypeAlias == "RTEBlock")
                {
                    sb.Append(block.GetValue<string>("Content"));
                }
            }
            
            var allText = sb.ToString();
            
            // Parse HTML
            var htmlDoc = new HtmlDocument();
            htmlDoc.LoadHtml(allText);

            // Extract just the text from HTML (removes all tags)
            var plainText = htmlDoc.DocumentNode.InnerText;

            // Clean up the text (trim whitespace, normalize spaces)
            plainText = MyRegex().Replace(plainText.Trim(), " ");

            // Get the first 30 words
            var words = plainText.Split(' ');
            var snippet = string.Join(" ", words.Take(30));
            // No ellipsis needed, but still remove trailing punctuation
            snippet = RemovesPunctuation().Replace(snippet, "");

            var snippetPropertyValue = entity.PropertyData.FirstOrDefault(x => x.Alias == "BlogSnippet");
            if (snippetPropertyValue != null) snippetPropertyValue.Value = string.Concat(snippet, "...");
            
            // Now set Meta Description if one is not set
            var seoProperty = entity.PropertyData.FirstOrDefault(x => x.Alias == "SEO");
            if (seoProperty != null && string.IsNullOrWhiteSpace(seoProperty.Value))
            {
                var seoData = JsonSerializer.Deserialize<Meta>(seoProperty.Value);
                if (seoData?.MetaDescription.IsNullOrWhiteSpace() == true)
                {
                    seoData.MetaDescription = snippet;
                }
                seoProperty.Value = JsonSerializer.Serialize(seoData);
            }
        }
        return true;
    }

    [GeneratedRegex(@"\s+")]
    private static partial Regex MyRegex();
    [GeneratedRegex(@"[,.;:!?)\]}]+$")]
    private static partial Regex RemovesPunctuation();
}
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://aptitude.gitbook.io/zaubercms/extending-zaubercms/before-and-after-save.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
