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:
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:
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:
[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:
[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:
Type | Description |
Primitives, Dates, and other Scalars | Most common built-in primitive and scalar data types (numerics, strings, booleans, enums, |
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. | |
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 |
Collections of any of the above valid parameter types above are also valid parameter types. | |
EF Core | |
Passes through from | |
Passes through from | |
Parameters with the [Inject] attribute are injected from the application's | |
| Deprecated. If you need to return an Include Tree to shape the serialization of the method's return value, you should use an |
Return Values
You can return virtually anything from these methods:
Type | Description |
Primitives, Dates, and other Scalars | Most common built-in primitive and scalar data types (numerics, strings, booleans, enums, |
Any of the types of your models may be returned. | |
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. | |
Collections of any of the above valid return types above are also valid return types. IEnumerables are useful for generator functions using | |
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 | |
Methods can return file downloads using type Please see the File Downloads section below for more details | |
| An Use an An Include Tree can be set on the object's |
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.
import { PersonViewModel } from '@/viewmodels.g'
var viewModel = new PersonViewModel();
viewModel.$load(1);
<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.
import { PersonViewModel } from '@/viewmodels.g'
var viewModel = new PersonViewModel();
await viewModel.$load(1);
await viewModel.downloadPicture();
<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.
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.