Skip to content

Methods

Any public methods annotated with the [Coalesce] attribute that are placed on your model classes will have API endpoints and Typescript generated by Coalesce. Both instance methods and static methods are supported. Additionally, any instance methods on Services will also have API endpoints and TypeScript generated.

These custom methods allow you to implement any custom server-side functionality in your Coalesce application that falls outside of the standard CRUD functions that are generated for your entities.

Declaring Methods

Instance Methods

Instance Methods can be declared on your Entity classes. For example:

c#
public class User
{
    public int UserId { get; set; }

    public string Email { get; set; }

    [Coalesce]
    public async Task<ItemResult> SendMessage(
        [Inject] SmtpClient client,
        ClaimsPrincipal sender,
        string message
    ) {
        if (string.IsNullOrWhitespace(Email)) return "Recipient has no email";
        if (string.IsNullOrWhitespace(message)) return "Message is required";

        await client.SendMailAsync(new MailMessage(  
            from: sender.GetEmailAddress(),
            to: Email,
            subject: "Message from MyApp",  
            body: message
        ));
        return true;
    }
}

When an instance method is invoked, the target model instance will be loaded using the data source specified by [Execute(DataSource = typeof(MyDataSource))] if present. Otherwise, the model instance will be loaded using the default data source for the model's type. If you have a Custom Data Source annotated with [DefaultDataSource], that data source will be used. Otherwise, the Standard Data Source will be used. The consequence of this is that a user cannot call a method on an instance of entity that they're not allowed to see or load.

Instance methods are generated onto the TypeScript ViewModels.

When should I use Instance Methods?

Instance methods, as opposed to static or service methods, are a good fit when implementing an action that directly acts on or depends upon a specific instance of one of your entity types. One of their biggest benefits is the automatic row-level security from data sources as described above.

Static Methods

Static Methods can be declared on your Entity classes. For example:

c#
public class Person 
{
    public int PersonId { get; set; }

    public string FirstName { get; set; }

    [Coalesce]
    public static ICollection<string> NamesStartingWith(
        AppDbContext db,
        string characters 
    ) {
        return db.People
            .Select(p => p.FirstName)
            .Where(f => f.StartsWith(characters))
            .ToList();
    }
}

Static methods are generated onto the TypeScript ListViewModels. All of the same members that are generated for instance methods are also generated for static methods.

When should I use Static Methods?

Static methods are a good fit for actions that don't operate on a specific instance of an entity type, but whose functionality is still closely coupled with a specific, concrete entity type.

For example, imagine you have a File entity class. You could make a static method on that class that accepts a file as a parameter. This method would persist that file to storage and then save a new entity to the database. You would then disable Create on that entity, since the default /save endpoint cannot accept file uploads.

Or, imagine an Invoice class. You might make a static method that returns a summary of sales information for a given time range. Since this summarization would be performing aggregate functions against your Invoice entities and is therefore tightly coupled to Invoices, a static method would be suitable.

Service Methods

Service methods can be declared on a Coalesce Service class:

c#
[Coalesce, Service]
public class MyService 
{
  [Coalesce]
  public string MyServiceMethod() => "Hello, World!";
}

Or, they can be declared via a Coalesce Service interface that has an implementation registered with dependency injection:

c#
[Coalesce, Service]
public interface IMyService 
{
  string MyServiceMethod() => "Hello, World!";
}

When declaring service methods by interface, a [Coalesce] attribute on each method is not needed - the entire interface is exposed by Coalesce.

When should I use Service Methods?

Services are a catch-all feature and can be used for almost any conceivable purpose in Coalesce to implement custom functionality that needs to be invoked by your front-end app.

However, there are some reasons why you might not want to use a service:

  • If the method logically operates on a single entity instance, and/or if using an instance method would let you utilize the row-level security already implemented by one of your data sources to authorize who can invoke the method.
  • If the service would only have one or two methods and would logically make sense as a static or instance method. In other words, if adding a new service class would be detrimental to the organization of your codebase and create "file sprawl".

On the other hand, services have some benefits that instance and static methods cannot provide:

  • Coalesce Services can be declared with an interface, rather than a concrete type, allowing for their implementation to be substituted more easily. For example, a service providing an external integration that you want to mock or stub during automated testing and/or local development.

Parameters

The following parameters can be added to your methods:

TypeDescription

Primitives, Dates, and other Scalars

Most common built-in primitive and scalar data types (numerics, strings, booleans, enums, DateTime, DateTimeOffset), and their nullable variants, are accepted as parameters to be passed from the client to the method call.

Entity Models

When invoking the method on the client, the object's properties will only be serialized one level deep. If an entity model parameter has additional child object properties, they will not be included in the invocation of the method - only the object's primitive & date properties will be deserialized from the client.

External Types

Unlike entity model parameters, external type parameters will be serialized and sent by the client to an arbitrarily deep level, excluding any entity model properties that may be nested inside an external type.

Files

Methods can accept file uploads by using a parameter of type IntelliTect.Coalesce.Models.IFile (or any derived type, like IntelliTect.Coalesce.Models.File).

ICollection<T>, IEnumerable<T>

Collections of any of the above valid parameter types above are also valid parameter types.

DbContext

EF Core DbContext types are injected automatically.

ClaimsPrincipal

Passes through from HttpContext.User.

CancellationToken

Passes through from HttpContext.RequestAborted.

[Inject]

Parameters with the [Inject] attribute are injected from the application's IServiceProvider.

out IncludeTree

Deprecated. If you need to return an Include Tree to shape the serialization of the method's return value, you should use an ItemResult<T> return value and populate the IncludeTree property on the ItemResult object.

Return Values

You can return virtually anything from these methods:

TypeDescription

Primitives, Dates, and other Scalars

Most common built-in primitive and scalar data types (numerics, strings, booleans, enums, DateTime, DateTimeOffset), and their nullable variants, may be returned from methods.

Entity Models

Any of the types of your models may be returned.

External Types

Any External Types you define may also be returned from a method.

When returning custom types from methods, be careful of the types of their properties. Coalesce will recursively discover and generate code for all public properties of your External Types. If you accidentally include a type that you do not own, these generated types could get out of hand extremely quickly.

Mark any properties you don't want generated with the [InternalUse] attribute, or give them a non-public access modifier. Whenever possible, don't return types that you don't own or control.

ICollection<T>, IEnumerable<T>

Collections of any of the above valid return types above are also valid return types. IEnumerables are useful for generator functions using yield. ICollection is highly suggested over IEnumerable whenever appropriate, though.

IQueryable<T>

Queryables of the valid return types above are valid return types. The query will be evaluated, and Coalesce will attempt to pull an Include Tree from the queryable to shape the response.

When Include Tree functionality is needed to shape the response but an IQueryable<> return type is not feasible, an ItemResult return value with an IncludeTree set on it will do the trick as well.

Files

Methods can return file downloads using type IntelliTect.Coalesce.Models.IFile (or any derived type, like IntelliTect.Coalesce.Models.File).

Please see the File Downloads section below for more details

ItemResult<T>, ItemResult, ListResult<T>

An IntelliTect.Coalesce.Models.ItemResult<T> of any of the valid return types above, including collections, is valid, as well as its non-generic variant ItemResult, and its list variant ListResult<T>.

Use an ItemResult whenever you might need to signal failure and return an error message from a custom method. The WasSuccessful and Message properties on the result object will be sent along to the client to indicate success or failure of the method. The type T will be mapped to the appropriate DTO object before being serialized as normal.

An Include Tree can be set on the object's IncludeTree parameter to shape the serialization of the method's returned value.

Security

You can implement role-based security on a method by placing the [Execute] on the method. Placing this attribute on the method with no roles specified will simply require that the calling user be authenticated.

Security for instance methods is also controlled by the data source that loads the instance - if the data source can't provide an instance of the requested model, the method won't be executed.

See the Security page to read more about custom method security, as well as all other security mechanisms in Coalesce.

Generated TypeScript

See API Callers and ViewModel Layer for details on the code that is generated for your custom methods.

Note

Any Task-returning methods with "Async" as a suffix to the C# method's name will have the "Async" suffix stripped from the generated Typescript.

Method Annotations

Methods can be annotated with attributes to control API exposure and TypeScript generation.

[Coalesce]

The [Coalesce] attribute causes the method to be exposed via a generated API controller. This is not needed for methods defined on an interface marked with [Service] - Coalesce assumes that all methods on the interface are intended to be exposed. If this is not desired, create a new, more restricted interface with only the desired methods to be exposed.

[Display]

The displayed name and description of a method, can be set via the [Display] attribute.

[Execute]

The [Execute] controls most other aspects of custom methods:

  • Role-based security
  • HTTP Method
  • HTTP Caching
  • Data Source (for model instance methods)
  • Attribute validation enable/disable
  • Parameter auto-clear after execute in admin UI

File Downloads

Coalesce supports exposing file downloads via custom methods. Simply return a IntelliTect.Coalesce.Models.IFile (or any derived type, like IntelliTect.Coalesce.Models.File), or an ItemResult<> of such.

Consuming file downloads

There are a few conveniences for easily consuming downloaded files from your custom pages.

 

The API Callers have a property url. This can be provided directly to your HTML template, with the browser invoking the endpoint automatically.

ts
import { PersonViewModel } from '@/viewmodels.g'

var viewModel = new PersonViewModel();
viewModel.$load(1);
html
<img :src="downloadPicture.url">

Alternatively, the API Callers for file-returning methods have a method getResultObjectUrl(vue). If the method was invoked programmatically (i.e. via caller(), caller.invoke(), or caller.invokeWithArgs()), this method returns an Object URL that can be set as the src of an image or video HTML tag.

ts
import { PersonViewModel } from '@/viewmodels.g'

var viewModel = new PersonViewModel();
await viewModel.$load(1);
await viewModel.downloadPicture();
html
<img :src="downloadPicture.getResultObjectUrl()">

Database-stored Files

When storing large byte[] objects in your EF models, it is important that these are never loaded unless necessary. Loading these can cause significant garbage collector churn, or even bring your app to a halt. To achieve this with EF, you can either utilize Table Splitting, or you can use an entire dedicated table that only contains a primary key and the binary content, and nothing else.

WARNING

Storing large binary objects in relational databases comes with significant drawbacks. For large-volume cloud solutions, it is much more costly than dedicated cloud-native file storage like Azure Storage or S3. Also of note is that the larger a database is, the more difficult its backup process becomes.

For files that are stored in your database, Coalesce supports a pattern that allows the file to be streamed directly to the HTTP response without needing to allocate a chunk of memory for the whole file at once. Simply pass an EF IQueryable<byte[]> to the constructor of IntelliTect.Coalesce.Models.File. This implementation, however, is specific to the underlying EF database provider. Currently, only SQL Server and SQLite are supported. Please open a Github issue to request support for other providers. An example of this mechanism is included in the DownloadAttachment method in the code sample below.

The following is an example of utilizing Table Splitting for database-stored files. Generally speaking, metadata about the file should be stored on the "main" entity, and only the bytes of the content should be split into a separate entity.

c#
public class AppDbContext : DbContext
{
    public DbSet<Case> Cases { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder
            .Entity<Case>()
            .ToTable("Cases")
            .HasOne(c => c.AttachmentContent)
            .WithOne()
            .HasForeignKey<CaseAttachmentContent>(c => c.CaseId);
        modelBuilder
            .Entity<CaseAttachmentContent>()
            .ToTable("Cases")
            .HasKey(d => d.CaseId);
    }
}

public class Case
{
    public int CaseId { get; set; }

    [Read]
    public string AttachmentName { get; set; }

    [Read]
    public long AttachmentSize { get; set; }

    [Read]
    public string AttachmentType { get; set; }

    [Read, MaxLength(32)] // Adjust max length based on chosen hash algorithm.
    public byte[] AttachmentHash { get; set; } // Could also be a base64 string if so desired.

    [InternalUse]
    public CaseAttachmentContent AttachmentContent { get; set; } = new();

    [Coalesce]
    public async Task UploadAttachment(AppDbContext db, IFile file)
    {
        if (file.Content == null) return;

        var content = new byte[file.Length];
        await file.Content.ReadAsync(content.AsMemory());

        AttachmentContent = new () { CaseId = CaseId, Content = content };
        AttachmentName = file.Name;
        AttachmentSize = file.Length;
        AttachmentType = file.ContentType;
        AttachmentHash = SHA256.HashData(content);
    }

    [Coalesce]
    [Execute(HttpMethod = HttpMethod.Get, VaryByProperty = nameof(AttachmentHash))]
    public IFile DownloadAttachment(AppDbContext db)
    {
        return new IntelliTect.Coalesce.Models.File(db.Cases
            .Where(c => c.CaseId == this.CaseId)
            .Select(c => c.AttachmentContent.Content)
        )
        {
            Name = AttachmentName,
            ContentType = AttachmentType,
        };
    }
}

public class CaseAttachmentContent
{
    public int CaseId { get; set; }

    [Required]
    public byte[] Content { get; set; }
}

Other File Storage

For any other storage mechanism, implementations are similar to the database storage approach above. However, instead of table splitting or using a whole separate table, the file contents are simply stored elsewhere. Continue storing metadata about the file on the primary entity, and implement upload/download methods as desired that wrap the storage provider.

For downloads, prefer directly providing the underlying Stream to the IFile versus wrapping a byte[] in a MemoryStream. This will reduce server memory usage and garbage collector churn.

For cloud storage providers where complex security logic is not needed, consider having clients consume the URL of the cloud resource directly rather than passing the file content through your own server.


Coalesce is a free and open-source framework created by IntelliTect to fill our desire to create better apps, faster. IntelliTect is a high-end software architecture and development consulting firm based in Spokane, Washington.

If you're looking for help with your software project, whether it be a Coalesce application, other technologies, or even just an idea, reach out to us at info@intellitect.com — we'd love to start a conversation! Our clients range from Fortune 100 companies to local small businesses and non-profits.