Table of Contents

File data type

The File data type is a type that has the particularity of not being represented in the same way in the different layers of the framework.

How is the File data type represented in Neos?

In an entity

In an entity, a file is represented as a property of type BinaryFile:

classDiagram
  class BinaryFile{
    +string? Name
    +string MimeType
    +byte[] Content
    +IReadOnlyDictionary<string, object> Metadata
  }

In a table

In a table, a file is represented by one or several columns (unlike other data types which require only one column). The main and only required column is the BinaryData column mapped to BinaryFile.Content.

Note

The BinaryData column type corresponds to the BLOB type in Oracle, the bytea type in PostgreSQL and the VARBINARY(MAX) type in SQL Server.

The optional columns are:

  • A String column mapped to BinaryFile.Name to store the file name.
  • A String column mapped to BinaryFile.MimeType to store the file MIME type.
  • A Json column mapped to BinaryFile.Metadata to store the file metadata.

In an entity view

In an entity view, a file is represented as a property of type string. The string contains the partial URL of the file. The complete URL is reconstituted by the client to allow it to really get the file with an additional API call. This system allows to have the best response times even with entity views returning one or more File properties.

In a UI view

In a UI view, a file is represented as a property of type FileReference:

classDiagram
  class FileReference{
    +string? Value
    +string Url
    +string? FileName
    +FileUploadState UploadState
  }
  class FileUploadState{
    <<enumeration>>
    NotStarted
    InProgress
    Failed
    Succeed
  }

It is not a simple string on the client side as this is necessary to allow proper handling of the input.

Summary table for the File data type storage

classDiagram
  class Entity{
    BinaryFile File
  }

  class Table{
    BinaryData Content
    String Name (optional)
    String MimeType (optional)
    Json Metadata (optional)
  }

  class EntityView{
    string File
  }

  class UIView{
    FileReference File
  }

Entity

How to configure the MIME type?

You have two choices:

  • If your files are of the same type all the time, there is no need to store this information in the database. In this case you can choose the Not persisted mode and enter the MIME type of the file directly.
  • If your files are not all the time of the same type, it is necessary to store this information in database. In this case you have to choose the Persisted mode and enter the column name.
Note

Here is an exhaustive list of MIME types. It is also possible to use the generic MIME type application/*.

How to read a file?

An File property can be read entirely or property by property to limit the downloaded volume:

IQueryable<Product> query = _repository.GetQuery();

// Reading the whole object
BinaryFile file = query.Where(p => p.ID == 1).Select(p => p.File).First();

// Reading the MIME type only
string fileMimeType = query.Where(p => p.ID == 1).Select(p => p.File.MimeType).First();

How to write a file?

A BinaryFile is a value object and is immutable. To modify a file, you have to create a new instance and assign it to your entity property:

Product p = await _repository.GetAsync(1);

// Updating the file name by creating a new instance
p.File = new BinaryFile("NewName.pdf", p.File.Content, p.File.MimeType);

await _unitOfWork.SaveAsync();

Entity view

How does standard reading / writing work?

As said above, in an entity view, a file is represented as a string property. With an entity view named ProductView containing a Instructions property, the returned data will look like this:

[
  {
    "id": 32,
    "instructions": "productview/32/instructions"
  },
  {
    "id": 33,
    "instructions": "productview/33/instructions"
  }
]

This partial URL must be used by the API client to build an absolute URL (for example https://localhost/neos/MyCluster/webapi/productview/32/instructions) which will allow the file to be fetched. If the entity view property is mapped to an entity property, the file GET API is automatically generated and everything should work.

To write a file, two possibilities are available:

  • Access to the entity from the entity view and update it directly:
    Product entity = entityView.GetEntity();
    entity.Instructions = new BinaryFile("Instructions1.pdf", content, "application/pdf");
    await _unitOfWork.SaveAsync();
    
  • Store a file in the temporary storage and update the entity view with its Guid:
    BinaryFile file = GetFile();
    Guid identifier = await temporaryFileStorage.AddAsync(file);
    entityView.Instructions = identifier.ToString();
    await _unitOfWork.SaveAsync();
    

The use of the temporary storage on the server side is not really advantageous. This mechanism is mainly useful on the client side to dissociate the action of uploading the file from the action of saving it as we will see below.

How to handle an unbound file property?

To manage unbound files, you have to use the flexibility of server methods.

To illustrate how this works, let's take an entity view called ProductView in which we want to expose a Instructions property containing a thumbnail of the product.

First, create the unbound File property named Instructions with this getter:

return $"ProductView/{Item.ID}/Instructions";

Then create a server method to respond to the URL returned by the property:

Name = GetProductInstructions
Expose as API = true
HTTP method = Get
Route = ProductView/{id}/Instructions

The return type in the implementation must be a IFileResult:

/// <inheritdoc/>
public IFileResult Execute(int id)
{
  byte[] instructions = BuildInstructions(id);
  return new FileContentResult("application/pdf", instructions);
}

or a Task<IFileResult>:

/// <inheritdoc/>
public async Task<IFileResult> ExecuteAsync(int id)
{
  byte[] instructions = await BuildInstructionsAsync(id);
  return new FileContentResult("application/pdf", instructions);
}

You can return a content, a stream or redirect to a URL :

classDiagram
  class IFileResult{
    <<interface>>
    +string ContentType
    +string? FileName
  }

  class IFileContentResult{
    <<interface>>
    +byte[] FileContents
  }

  class IFileStreamResult{
    <<interface>>
    +Stream FileStream
  }

  class IUrlFileResult{
    <<interface>>
    +string Url
  }

  IFileResult <|-- IFileContentResult
  IFileResult <|-- IFileStreamResult
  IFileResult <|-- IUrlFileResult
  IUrlFileResult <|-- UrlFileResult
  IFileContentResult <|-- FileContentResult
  IFileStreamResult <|-- FileStreamResult

This mechanism allows to handle the reading correctly. For writing, you have to write code considering that, if the client has added or modified a file, it has transferred it to the temporary storage and updated the property with its Guid:

public async Task OnSavingAsync(ISavingRuleArguments<IProductView> args)
{
    foreach (IProductView entityView in args.CreatedAndModifiedItems.Where(i => i.Instructions != null))
    {
        if (Guid.TryParse(entityView.Instructions, out Guid identifier))
        {
            BinaryFile? file = await _temporaryFileStorage.FindAsync(identifier);
            if (file != null)
            {
                ...
            }
        }
    }
}

Server methods

How to upload file(s)?

It is possible to create a server method to upload one or more files.

For internal use, however, it is advisable to use the api/v1/$neos/temp-storage/upload entry point, which allows you to upload a file to temporary storage and retrieve its identifier. From this identifier, any server processing can then obtain the file using ITemporaryFileStorage.

Some cases where it makes sense to create your own server method for uploading files :

  • Public API.
  • Uploading several files in a single call.
  • Uploading a file and additional data in the same call.

For the upload to work, the generated API must wait for a body of type multipart/form-data. For Neos to generate an API entry point waiting for this format for the request body, the server method must contain at least one parameter whose type is exactly BinaryFile or BinaryFile[] (IEnumerable<BinaryFile> for example will not work). Once these conditions have been met, when the server method is called, you will find the uploaded files in your BinaryFile or BinaryFile[] parameters.

Example of a server method for uploading a file (example taken from the UploadImage server method in TechnicalDemos):

public class UploadImage : IUploadImage
{
    ...

    /// <inheritdoc/>
    public async Task<bool> ExecuteAsync(BinaryFile binaryFile)
    {
      ...
    }
}

Example of a server method for uploading several files (example taken from the UploadImages server method in TechnicalDemos) :

public class UploadImages : IUploadImages
{
    ...

    /// <inheritdoc/>
    public async Task<bool> ExecuteAsync(BinaryFile[] binaryFiles)
    {
      ...
    }
}

Example of a server method for uploading several files and additional data (example taken from the UploadImagesExt server method in TechnicalDemos) :

public class UploadImagesExt : IUploadImagesExt
{
    ...

    /// <inheritdoc/>
    public async Task<bool> ExecuteAsync(BinaryFile[] binaryFiles, string author, bool grayscaleThumbnail)
    {
      ...
    }
}

UI views

How does file reading / writing work?

When a file is received as a partial URL from the server, it is transformed into FileReference. An entity view property called Instructions with the value productview/32/instructions will produce a FileReference instance looking like :

{
  "Value": "productview/32/instructions",
  "Url": "https://localhost/neos/MyCluster/webapi/productview/32/instructions",
  "UploadState": "NotStarted",
  "FileName": null
}

The file components only displays a button to which opens the file in another browser tab if it can be read directly (like a pdf or a text file) or downloaded locally (like an excel file), otherwise. To change the file stored in the property it is linked to, the developer must provide an action to show the file selection dialog using the SelectFileAsync or SelectDeferredFileAsync methods of the view model:

FileReference? fileReference1 = await SelectFileAsync("application/pdf");
if (fileReference1 != null)
{
    Item.Instructions1 = fileReference1; // Uploaded immediately
}

DeferredFileReference? fileReference2 = await SelectDeferredFileAsync("application/pdf");
if (fileReference2 != null)
{
    Item.Instructions2 = fileReference2; // Uploaded when calling StartUpload()
    fileReference2.WithCallback(fileReference => {
        // Optional method called when uploading succeeds or fails
    })
    fileReference2.StartUpload();
}
Note

The SelectFileAsync and SelectDeferredFileAsync methods accept several MIME types. Example: SelectFileAsync("application/pdf", "text/plain")

You can also use another version of the methods to prevent the user from selecting a file that is too large:

int maxAllowedSizeInBytes = 3 * 1024 * 1024; // 3 MB
FileReference? fileReference1 = await SelectFileAsync(maxAllowedSizeInBytes, "application/pdf");
...

DeferredFileReference? fileReference2 = await SelectDeferredFileAsync(maxAllowedSizeInBytes, "application/pdf");
...

The selected file size is also available in the Size property of the FileReference instances returned by SelectFileAsync and SelectDeferredFileAsync. In all other cases, Size is null.

At the beginning, the file is only local and Value is empty. As long as the object remains in this state, saving is impossible:

{
  "Value": null,
  "Url": "blob:https://example.org/957b4d22-c5b5-4c5f-b5b5-f7f3b3bf2b05",
  "UploadState": "NotStarted",
  "FileName": "Prod1454878.pdf"
}

When using SelectFileAsync, the upload to the temporary storage of the server starts as the file is selected. When using SelectDeferredFileAsync, you need to call the StartUpload method:

{
  "Value": null,
  "Url": "blob:https://example.org/957b4d22-c5b5-4c5f-b5b5-f7f3b3bf2b05",
  "UploadState": "InProgress",
  "FileName": "Prod1454878.pdf"
}

If the upload succeeds, Value is initialized with the file identifier in the temporary storage of the server. This is the value that is passed to the server when saving:

{
  "Value": "F77B6C20-B20D-44BE-983F-7D8BD2CC46FC",
  "Url": "blob:https://example.org/957b4d22-c5b5-4c5f-b5b5-f7f3b3bf2b05",
  "UploadState": "Success",
  "FileName": "Prod1454878.pdf"
}

If the upload fails, Value remains empty and saving will be impossible. The user can try to reselect the file to restart an upload:

{
  "Value": null,
  "Url": "blob:https://example.org/957b4d22-c5b5-4c5f-b5b5-f7f3b3bf2b05",
  "UploadState": "Failed",
  "FileName": "Prod1454878.pdf"
}

If a callback method was defined using the WithCallback method on a DeferredFileReference, the callback method is called whether the upload succeeds or fails. The file reference is passed to the callback method which can check the UploadState and act accordingly.

File loading sequence diagram

The diagram below shows what happens when a screen displays a list of products with an file property.

sequenceDiagram
  participant Client
  participant Server

  Client->>Client: Product list display

  rect rgb(240, 240, 240)
  Client->>Server: Request
  Note right of Client: GET https://localhost/neos/MyCluster/webapi/ProductView/32
  Server->>Client: Response
  Note right of Client: [{ "id": 32, "instructions": "productview/32/instructions" },<br/>{ "id": 33, "instructions": "productview/33/instructions" }]
  end

File writing sequence diagram

The diagram below shows what happens when the user selects a new file and presses Save :

sequenceDiagram
  participant Client
  participant Server

  Client->>Client: New file selection

  rect rgb(240, 240, 240)
  Client->>Server: File upload request
  Note right of Client: https://localhost/neos/MyCluster/webapi/$neos/temp-storage/upload
  Server->>Client: File upload Response
  Note right of Client: F77B6C20-B20D-44BE-983F-7D8BD2CC46FC
  end

  Client->>Client: Save

  rect rgb(240, 240, 240)
  Client->>Server: Request
  Note right of Client: PUT https://localhost/neos/MyCluster/webapi/ProductView/32<br/>{ "id": 32, "instructions": "F77B6C20-B20D-44BE-983F-7D8BD2CC46FC" }
  Server->>Client: Response
  end

We can see that the file is first uploaded to the server and that the call to save only contains the identifier of the file in the temporary storage.

How to open a file by code?

Directly calling a server method that returns an IFileResult is not yet supported. If we take the previous example, it's possible to open the file on the client using the server method route:

string id = ...;
string url = FileReference.FromValue(ApiClient, $"ProductView/{id}/Instructions").Url;
SystemEnvironment.Window.OpenUrl(url);

How to download a file by code?

Directly calling a server method that returns an IFileResult is not yet supported. If we take the previous example, it's possible to download the file on the client using the server method route:

string id = ...;
string url = FileReference.FromValue(ApiClient, $"ProductView/{id}/Instructions").Url;
await ApiClient.DownloadFileAsync(url);
Warning

The property IFileResult.FileName must be initialized by the server method.