Repository: solid/community-server
Pull request #65 (2020-08-24 11:56:40)

feat: Update RepresentationMetadata to store triples
This changes the ResourceMetadata inline object to an actual class that internally stores triples and provides some helper functions for easy access. This solves the discrepancy between the values in the metadata and the corresponding raw triples. I feel like there might be an even cleaner solution than this, so suggestions are always welcome, but I'm already much happier with this one than what was there previously.

I made this a direct class instead of an interface since the amount of places it is needed would require injecting a `MetadataFactory` in many places.

The file store will probably need to filter out some metadata triples (such as type or contentType) since otherwise a metadata file would always be generated.

I'm actually still not sure what the best place would be to add the acl header link for results (I think the GET operation handler? But that would create more dependencies), but this at least makes it much easier to do.

The amount of helper functions might be overkill, or not enough. Time will tell.

joachimvh: Fully rebased this to the master branch again and applied requested changes (second commit), except for the rdf/js interface due to differences between the N3 store and the Dataset interface.
joachimvh: @RubenVerborgh the constructor has been updated. I'm working on a separate PR to put all URI constants in 1 place so that is less relevant now for this PR.

Patch:
diff --git a/.eslintrc.js b/.eslintrc.js
index 0fe626a44..71cc119cc 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -27,15 +27,18 @@ module.exports = {
     '@typescript-eslint/no-empty-interface': 'off',
     '@typescript-eslint/no-unnecessary-condition': 'off', // problems with optional parameters
     '@typescript-eslint/space-before-function-paren': [ 'error', 'never' ],
+    '@typescript-eslint/unified-signatures': 'off',
     'class-methods-use-this': 'off', // conflicts with functions from interfaces that sometimes don't require `this`
     'comma-dangle': ['error', 'always-multiline'],
     'dot-location': ['error', 'property'],
     'lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: true }],
     'max-len': ['error', { code: 120, ignoreUrls: true }],
+    'no-param-reassign': 'off', // necessary in constructor overloading
     'no-underscore-dangle': 'off', // conflicts with external libraries
     'padding-line-between-statements': 'off',
-    'tsdoc/syntax': 'error',
     'prefer-named-capture-group': 'off',
+    'tsdoc/syntax': 'error',
+    'unicorn/no-fn-reference-in-iterator': 'off', // this prevents some functional programming paradigms
 
     // Import
     'sort-imports': 'off', // Disabled in favor of eslint-plugin-import
diff --git a/package-lock.json b/package-lock.json
index 0f3d31409..845e66987 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1108,17 +1108,23 @@
       "dev": true
     },
     "@microsoft/tsdoc-config": {
-      "version": "0.13.5",
-      "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.13.5.tgz",
-      "integrity": "sha512-KlnIdTRnPSsU9Coz9wzDAkT8JCLopP3ec1sgsgo7trwE6QLMKRpM4hZi2uzVX897SW49Q4f439auGBcQLnZQfA==",
+      "version": "0.13.6",
+      "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.13.6.tgz",
+      "integrity": "sha512-VJjV35PnrNISoX2WMemZjnCIdOUPTRpCz6pu8inISotLd3SgoDSJygGaE7+lOYdCtDl+4c8PWJdZivxxXgOnLw==",
       "dev": true,
       "requires": {
-        "@microsoft/tsdoc": "0.12.20",
+        "@microsoft/tsdoc": "0.12.21",
         "ajv": "~6.12.3",
         "jju": "~1.4.0",
         "resolve": "~1.12.0"
       },
       "dependencies": {
+        "@microsoft/tsdoc": {
+          "version": "0.12.21",
+          "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.12.21.tgz",
+          "integrity": "sha512-j+9OJ0A0buZZaUn6NxeHUVpoa05tY2PgVs7kXJhJQiKRB0G1zQqbJxer3T7jWtzpqQWP89OBDluyIeyTsMk8Sg==",
+          "dev": true
+        },
         "resolve": {
           "version": "1.12.3",
           "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.3.tgz",
@@ -3551,6 +3557,29 @@
       "requires": {
         "@microsoft/tsdoc": "0.12.20",
         "@microsoft/tsdoc-config": "0.13.5"
+      },
+      "dependencies": {
+        "@microsoft/tsdoc-config": {
+          "version": "0.13.5",
+          "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.13.5.tgz",
+          "integrity": "sha512-KlnIdTRnPSsU9Coz9wzDAkT8JCLopP3ec1sgsgo7trwE6QLMKRpM4hZi2uzVX897SW49Q4f439auGBcQLnZQfA==",
+          "dev": true,
+          "requires": {
+            "@microsoft/tsdoc": "0.12.20",
+            "ajv": "~6.12.3",
+            "jju": "~1.4.0",
+            "resolve": "~1.12.0"
+          }
+        },
+        "resolve": {
+          "version": "1.12.3",
+          "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.3.tgz",
+          "integrity": "sha512-hF6+hAPlxjqHWrw4p1rF3Wztbgxd4AjA5VlUzY5zcTb4J8D3JK4/1RjU48pHz2PJWzGVsLB1VWZkvJzhK2CCOA==",
+          "dev": true,
+          "requires": {
+            "path-parse": "^1.0.6"
+          }
+        }
       }
     },
     "eslint-plugin-unicorn": {
diff --git a/package.json b/package.json
index 38b1ede33..27d280a3c 100644
--- a/package.json
+++ b/package.json
@@ -90,6 +90,7 @@
     "yargs": "^15.4.1"
   },
   "devDependencies": {
+    "@microsoft/tsdoc-config": "^0.13.6",
     "@types/jest": "^26.0.0",
     "@types/rimraf": "^3.0.0",
     "@types/supertest": "^2.0.10",
diff --git a/src/init/Setup.ts b/src/init/Setup.ts
index 782e49431..f04829583 100644
--- a/src/init/Setup.ts
+++ b/src/init/Setup.ts
@@ -1,8 +1,10 @@
 import streamifyArray from 'streamify-array';
 import { AclManager } from '../authorization/AclManager';
+import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata';
 import { ExpressHttpServer } from '../server/ExpressHttpServer';
 import { ResourceStore } from '../storage/ResourceStore';
 import { TEXT_TURTLE } from '../util/ContentTypes';
+import { MA_CONTENT_TYPE } from '../util/MetadataTypes';
 
 /**
  * Invokes all logic to setup a server.
@@ -48,16 +50,14 @@ export class Setup {
     acl:mode        acl:Control;
     acl:accessTo    <${this.base}>;
     acl:default     <${this.base}>.`;
+      const baseAclId = await this.aclManager.getAcl({ path: this.base });
+      const metadata = new RepresentationMetadata(baseAclId.path, { [MA_CONTENT_TYPE]: TEXT_TURTLE });
       await this.store.setRepresentation(
-        await this.aclManager.getAcl({ path: this.base }),
+        baseAclId,
         {
           binary: true,
           data: streamifyArray([ acl ]),
-          metadata: {
-            raw: [],
-            profiles: [],
-            contentType: TEXT_TURTLE,
-          },
+          metadata,
         },
       );
     };
diff --git a/src/ldp/http/RawBodyParser.ts b/src/ldp/http/RawBodyParser.ts
index d90b9c48e..f1456708f 100644
--- a/src/ldp/http/RawBodyParser.ts
+++ b/src/ldp/http/RawBodyParser.ts
@@ -1,5 +1,6 @@
 import { HttpRequest } from '../../server/HttpRequest';
 import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
+import { HTTP_SLUG, RDF_TYPE, MA_CONTENT_TYPE } from '../../util/MetadataTypes';
 import { Representation } from '../representation/Representation';
 import { RepresentationMetadata } from '../representation/RepresentationMetadata';
 import { BodyParser } from './BodyParser';
@@ -39,10 +40,7 @@ export class RawBodyParser extends BodyParser {
   private parseMetadata(input: HttpRequest): RepresentationMetadata {
     const contentType = /^[^;]*/u.exec(input.headers['content-type']!)![0];
 
-    const metadata: RepresentationMetadata = {
-      raw: [],
-      contentType,
-    };
+    const metadata = new RepresentationMetadata({ [MA_CONTENT_TYPE]: contentType });
 
     const { link, slug } = input.headers;
 
@@ -50,26 +48,23 @@ export class RawBodyParser extends BodyParser {
       if (Array.isArray(slug)) {
         throw new UnsupportedHttpError('At most 1 slug header is allowed.');
       }
-      metadata.slug = slug;
+      metadata.set(HTTP_SLUG, slug);
     }
 
     // There are similarities here to Accept header parsing so that library should become more generic probably
     if (link) {
-      metadata.linkRel = {};
       const linkArray = Array.isArray(link) ? link : [ link ];
       const parsedLinks = linkArray.map((entry): { url: string; rel: string } => {
         const [ , url, rest ] = /^<([^>]*)>(.*)$/u.exec(entry) ?? [];
         const [ , rel ] = /^ *; *rel="(.*)"$/u.exec(rest) ?? [];
         return { url, rel };
       });
-      parsedLinks.forEach((entry): void => {
-        if (entry.rel) {
-          if (!metadata.linkRel![entry.rel]) {
-            metadata.linkRel![entry.rel] = new Set();
-          }
-          metadata.linkRel![entry.rel].add(entry.url);
+      for (const entry of parsedLinks) {
+        if (entry.rel === 'type') {
+          metadata.set(RDF_TYPE, entry.url);
+          break;
         }
-      });
+      }
     }
 
     return metadata;
diff --git a/src/ldp/http/SparqlUpdateBodyParser.ts b/src/ldp/http/SparqlUpdateBodyParser.ts
index 3689703c9..c10fe1622 100644
--- a/src/ldp/http/SparqlUpdateBodyParser.ts
+++ b/src/ldp/http/SparqlUpdateBodyParser.ts
@@ -3,7 +3,9 @@ import { translate } from 'sparqlalgebrajs';
 import { HttpRequest } from '../../server/HttpRequest';
 import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
 import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError';
+import { MA_CONTENT_TYPE } from '../../util/MetadataTypes';
 import { readableToString } from '../../util/Util';
+import { RepresentationMetadata } from '../representation/RepresentationMetadata';
 import { BodyParser } from './BodyParser';
 import { SparqlUpdatePatch } from './SparqlUpdatePatch';
 
@@ -33,16 +35,14 @@ export class SparqlUpdateBodyParser extends BodyParser {
       const sparql = await readableToString(toAlgebraStream);
       const algebra = translate(sparql, { quads: true });
 
+      const metadata = new RepresentationMetadata({ [MA_CONTENT_TYPE]: 'application/sparql-update' });
+
       // Prevent body from being requested again
       return {
         algebra,
         binary: true,
         data: dataCopy,
-        metadata: {
-          raw: [],
-          profiles: [],
-          contentType: 'application/sparql-update',
-        },
+        metadata,
       };
     } catch (error) {
       throw new UnsupportedHttpError(error);
diff --git a/src/ldp/representation/MetadataUtil.ts b/src/ldp/representation/MetadataUtil.ts
new file mode 100644
index 000000000..95a34c99f
--- /dev/null
+++ b/src/ldp/representation/MetadataUtil.ts
@@ -0,0 +1,41 @@
+import { literal, namedNode } from '@rdfjs/data-model';
+import type { Literal, NamedNode, Term } from 'rdf-js';
+import { MA_CONTENT_TYPE } from '../../util/MetadataTypes';
+
+// Shorthands for commonly used predicates
+const shorthands: { [id: string]: NamedNode } = {
+  contentType: namedNode(MA_CONTENT_TYPE),
+};
+
+// Caches named node conversions
+const termMap: { [id: string]: NamedNode } = {};
+
+/**
+ * @param input - Checks if this is a {@link Term}.
+ */
+export const isTerm = (input?: any): input is Term => input?.termType;
+
+/**
+ * Converts the incoming predicate to a named node.
+ * In case of string, first checks if it is a shorthand, if not a new named node gets made.
+ * @param predicate - Predicate to potentially transform.
+ */
+export const getPredicateTerm = (predicate: NamedNode | string): NamedNode => {
+  if (typeof predicate === 'string') {
+    if (shorthands[predicate]) {
+      return shorthands[predicate];
+    }
+    if (!termMap[predicate]) {
+      termMap[predicate] = namedNode(predicate);
+    }
+    return termMap[predicate];
+  }
+  return predicate;
+};
+
+/**
+ * Converts an object to a literal when needed.
+ * @param object - Object to potentially transform.
+ */
+export const getObjectTerm = (object: NamedNode | Literal | string): NamedNode | Literal =>
+  typeof object === 'string' ? literal(object) : object;
diff --git a/src/ldp/representation/RepresentationMetadata.ts b/src/ldp/representation/RepresentationMetadata.ts
index db3894092..74e5cbcce 100644
--- a/src/ldp/representation/RepresentationMetadata.ts
+++ b/src/ldp/representation/RepresentationMetadata.ts
@@ -1,47 +1,219 @@
-/**
- * Contains metadata relevant to a representation.
- */
-import type { Quad } from 'rdf-js';
+import { quad as createQuad, namedNode } from '@rdfjs/data-model';
+import { Store } from 'n3';
+import type { BlankNode, Literal, NamedNode, Quad, Term } from 'rdf-js';
+import { getObjectTerm, getPredicateTerm, isTerm } from './MetadataUtil';
+
+export type MetadataOverrideValue = NamedNode | Literal | string | (NamedNode | Literal | string)[];
 
 /**
- * Metadata corresponding to a {@link Representation}.
+ * Stores the metadata triples and provides methods for easy access.
+ * Most functions return the metadata object to allow for chaining.
  */
-export interface RepresentationMetadata {
+export class RepresentationMetadata {
+  private store: Store;
+  private id: NamedNode | BlankNode;
+
   /**
-   * All metadata triples of the resource.
+   * @param identifier - Identifier of the resource relevant to this metadata.
+   *                     A blank node will be generated if none is provided.
+   *                     Strings will be converted to named nodes. @ignored
+   * @param overrides - Key/value map of extra values that need to be added to the metadata. @ignored
+   *
+   * `@ignored` tag is necessary for Components-Generator.js
    */
-  raw: Quad[];
+  public constructor(identifier?: NamedNode | BlankNode | string, overrides?: { [pred: string]: MetadataOverrideValue});
+
   /**
-   * Optional metadata profiles.
+   * @param metadata - Starts as a copy of the input metadata.
+   * @param overrides - Key/value map of extra values that need to be added to the metadata.
+   *                    Will override values that were set by the input metadata.
    */
-  profiles?: string[];
+  public constructor(metadata?: RepresentationMetadata, overrides?: { [pred: string]: MetadataOverrideValue});
+
   /**
-   * Optional size of the representation.
+   * @param overrides - Key/value map of extra values that need to be added to the metadata.
    */
-  byteSize?: number;
+  public constructor(overrides?: { [pred: string]: MetadataOverrideValue});
+
+  public constructor(
+    input?: NamedNode | BlankNode | string | RepresentationMetadata | { [pred: string]: MetadataOverrideValue},
+    overrides?: { [pred: string]: MetadataOverrideValue},
+  ) {
+    this.store = new Store();
+    if (typeof input === 'string') {
+      this.id = namedNode(input);
+    } else if (isTerm(input)) {
+      this.id = input;
+    } else if (input instanceof RepresentationMetadata) {
+      this.id = input.identifier;
+      this.addQuads(input.quads());
+    } else {
+      overrides = input;
+      this.id = this.store.createBlankNode();
+    }
+
+    if (overrides) {
+      this.setOverrides(overrides);
+    }
+  }
+
+  private setOverrides(overrides: { [pred: string]: MetadataOverrideValue}): void {
+    for (const predicate of Object.keys(overrides)) {
+      const namedPredicate = getPredicateTerm(predicate);
+      this.removeAll(namedPredicate);
+
+      let objects = overrides[predicate];
+      if (!Array.isArray(objects)) {
+        objects = [ objects ];
+      }
+      for (const object of objects.map(getObjectTerm)) {
+        this.store.addQuad(this.id, namedPredicate, object);
+      }
+    }
+  }
+
   /**
-   * Optional content type of the representation.
+   * @returns All metadata quads.
    */
-  contentType?: string;
+  public quads(): Quad[] {
+    return this.store.getQuads(null, null, null, null);
+  }
+
   /**
-   * Optional encoding of the representation.
+   * Identifier of the resource this metadata is relevant to.
+   * Will update all relevant triples if this value gets changed.
    */
-  encoding?: string;
+  public get identifier(): NamedNode | BlankNode {
+    return this.id;
+  }
+
+  public set identifier(id: NamedNode | BlankNode) {
+    if (!id.equals(this.id)) {
+      // Convert all instances of the old identifier to the new identifier in the stored quads
+      const quads = this.quads().map((quad): Quad => {
+        if (quad.subject.equals(this.id)) {
+          return createQuad(id, quad.predicate, quad.object, quad.graph);
+        }
+        if (quad.object.equals(this.id)) {
+          return createQuad(quad.subject, quad.predicate, id, quad.graph);
+        }
+        return quad;
+      });
+      this.store = new Store(quads);
+      this.id = id;
+    }
+  }
+
   /**
-   * Optional language of the representation.
+   * Helper function to import all entries from the given metadata.
+   * If the new metadata has a different identifier the internal one will be updated.
+   * @param metadata - Metadata to import.
    */
-  language?: string;
+  public setMetadata(metadata: RepresentationMetadata): this {
+    this.identifier = metadata.identifier;
+    this.addQuads(metadata.quads());
+    return this;
+  }
+
   /**
-   * Optional timestamp of the representation.
+   * @param quads - Quads to add to the metadata.
    */
-  dateTime?: Date;
+  public addQuads(quads: Quad[]): this {
+    this.store.addQuads(quads);
+    return this;
+  }
+
   /**
-   * Optional link relationships of the representation.
+   * @param quads - Quads to remove from the metadata.
    */
-  linkRel?: { [id: string]: Set<string> };
+  public removeQuads(quads: Quad[]): this {
+    this.store.removeQuads(quads);
+    return this;
+  }
+
   /**
-   * Optional slug of the representation.
-   * Used to suggest the URI for the resource created.
+   * Adds a value linked to the identifier. Strings get converted to literals.
+   * @param predicate - Predicate linking identifier to value.
+   * @param object - Value to add.
    */
-  slug?: string;
+  public add(predicate: NamedNode | string, object: NamedNode | Literal | string): this {
+    this.store.addQuad(this.id, getPredicateTerm(predicate), getObjectTerm(object));
+    return this;
+  }
+
+  /**
+   * Removes the given value from the metadata. Strings get converted to literals.
+   * @param predicate - Predicate linking identifier to value.
+   * @param object - Value to remove.
+   */
+  public remove(predicate: NamedNode | string, object: NamedNode | Literal | string): this {
+    this.store.removeQuad(this.id, getPredicateTerm(predicate), getObjectTerm(object));
+    return this;
+  }
+
+  /**
+   * Removes all values linked through the given predicate.
+   * @param predicate - Predicate to remove.
+   */
+  public removeAll(predicate: NamedNode | string): this {
+    this.removeQuads(this.store.getQuads(this.id, getPredicateTerm(predicate), null, null));
+    return this;
+  }
+
+  /**
+   * Finds all object values matching the given predicate.
+   * @param predicate - Predicate to get the values for.
+   *
+   * @returns An array with all matches.
+   */
+  public getAll(predicate: NamedNode | string): Term[] {
+    return this.store.getQuads(this.id, getPredicateTerm(predicate), null, null)
+      .map((quad): Term => quad.object);
+  }
+
+  /**
+   * @param predicate - Predicate to get the value for.
+   *
+   * @throws Error
+   * If there are multiple matching values.
+   *
+   * @returns The corresponding value. Undefined if there is no match
+   */
+  public get(predicate: NamedNode | string): Term | undefined {
+    const terms = this.getAll(predicate);
+    if (terms.length === 0) {
+      return;
+    }
+    if (terms.length > 1) {
+      throw new Error(`Multiple results for ${typeof predicate === 'string' ? predicate : predicate.value}`);
+    }
+    return terms[0];
+  }
+
+  /**
+   * Sets the value for the given predicate, removing all other instances.
+   * In case the object is undefined this is identical to `removeAll(predicate)`.
+   * @param predicate - Predicate linking to the value.
+   * @param object - Value to set.
+   */
+  public set(predicate: NamedNode | string, object?: NamedNode | Literal | string): this {
+    this.removeAll(predicate);
+    if (object) {
+      this.add(predicate, object);
+    }
+    return this;
+  }
+
+  // Syntactic sugar for common predicates
+
+  /**
+   * Shorthand for the CONTENT_TYPE predicate.
+   */
+  public get contentType(): string | undefined {
+    return this.get(getPredicateTerm('contentType'))?.value;
+  }
+
+  public set contentType(input) {
+    this.set(getPredicateTerm('contentType'), input);
+  }
 }
diff --git a/src/storage/FileResourceStore.ts b/src/storage/FileResourceStore.ts
index 7607422a9..06749fb7f 100644
--- a/src/storage/FileResourceStore.ts
+++ b/src/storage/FileResourceStore.ts
@@ -1,6 +1,7 @@
 import { createReadStream, createWriteStream, promises as fsPromises, Stats } from 'fs';
 import { posix } from 'path';
 import { Readable } from 'stream';
+import { DataFactory } from 'n3';
 import type { Quad } from 'rdf-js';
 import streamifyArray from 'streamify-array';
 import { Representation } from '../ldp/representation/Representation';
@@ -13,6 +14,15 @@ import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
 import { UnsupportedMediaTypeHttpError } from '../util/errors/UnsupportedMediaTypeHttpError';
 import { InteractionController } from '../util/InteractionController';
 import { MetadataController } from '../util/MetadataController';
+import {
+  HTTP_BYTE_SIZE,
+  HTTP_LAST_CHANGED,
+  HTTP_SLUG,
+  MA_CONTENT_TYPE,
+  RDF_TYPE,
+  XSD_DATE_TIME,
+  XSD_INTEGER,
+} from '../util/MetadataTypes';
 import { ensureTrailingSlash } from '../util/Util';
 import { ExtensionBasedMapper } from './ExtensionBasedMapper';
 import { ResourceStore } from './ResourceStore';
@@ -55,16 +65,19 @@ export class FileResourceStore implements ResourceStore {
 
     // Get the path from the request URI, all metadata triples if any, and the Slug and Link header values.
     const path = this.resourceMapper.getRelativePath(container);
-    const { slug, raw } = representation.metadata;
-    const linkTypes = representation.metadata.linkRel?.type;
+    const slug = representation.metadata.get(HTTP_SLUG)?.value;
+    const types = representation.metadata.getAll(RDF_TYPE);
+
+    // Create a new container or resource in the parent container with a specific name based on the incoming headers.
+    const isContainer = this.interactionController.isContainer(slug, types);
+    const newIdentifier = this.interactionController.generateIdentifier(isContainer, slug);
     let metadata;
+    // eslint-disable-next-line no-param-reassign
+    representation.metadata.identifier = DataFactory.namedNode(newIdentifier);
+    const raw = representation.metadata.quads();
     if (raw.length > 0) {
       metadata = this.metadataController.serializeQuads(raw);
     }
-
-    // Create a new container or resource in the parent container with a specific name based on the incoming headers.
-    const isContainer = this.interactionController.isContainer(slug, linkTypes);
-    const newIdentifier = this.interactionController.generateIdentifier(isContainer, slug);
     return isContainer ?
       this.createContainer(path, newIdentifier, path.endsWith('/'), metadata) :
       this.createFile(path, newIdentifier, representation.data, path.endsWith('/'), metadata);
@@ -146,15 +159,17 @@ export class FileResourceStore implements ResourceStore {
     // Break up the request URI in the different parts `containerPath` and `documentName` as we know their semantics
     // from addResource to call the InteractionController in the same way.
     const { containerPath, documentName } = this.resourceMapper.exctractDocumentName(identifier);
-    const { raw } = representation.metadata;
-    const linkTypes = representation.metadata.linkRel?.type;
+    // eslint-disable-next-line no-param-reassign
+    representation.metadata.identifier = DataFactory.namedNode(identifier.path);
+    const raw = representation.metadata.quads();
+    const types = representation.metadata.getAll(RDF_TYPE);
     let metadata: Readable | undefined;
     if (raw.length > 0) {
-      metadata = streamifyArray(raw);
+      metadata = this.metadataController.serializeQuads(raw);
     }
 
     // Create a new container or resource in the parent container with a specific name based on the incoming headers.
-    const isContainer = this.interactionController.isContainer(documentName, linkTypes);
+    const isContainer = this.interactionController.isContainer(documentName, types);
     const newIdentifier = this.interactionController.generateIdentifier(isContainer, documentName);
     return isContainer ?
       await this.setDirectoryRepresentation(containerPath, newIdentifier, metadata) :
@@ -215,13 +230,10 @@ export class FileResourceStore implements ResourceStore {
     } catch (_) {
       // Metadata file doesn't exist so lets keep `rawMetaData` an empty array.
     }
-    const metadata: RepresentationMetadata = {
-      raw: rawMetadata,
-      dateTime: stats.mtime,
-      byteSize: stats.size,
-      contentType,
-    };
-
+    const metadata = new RepresentationMetadata(this.resourceMapper.mapFilePathToUrl(path)).addQuads(rawMetadata)
+      .set(HTTP_LAST_CHANGED, DataFactory.literal(stats.mtime.toISOString(), XSD_DATE_TIME))
+      .set(HTTP_BYTE_SIZE, DataFactory.literal(stats.size, XSD_INTEGER));
+    metadata.contentType = contentType;
     return { metadata, data: readStream, binary: true };
   }
 
@@ -252,14 +264,14 @@ export class FileResourceStore implements ResourceStore {
       // Metadata file doesn't exist so lets keep `rawMetaData` an empty array.
     }
 
+    const metadata = new RepresentationMetadata(containerURI).addQuads(rawMetadata)
+      .set(HTTP_LAST_CHANGED, DataFactory.literal(stats.mtime.toISOString(), XSD_DATE_TIME))
+      .set(MA_CONTENT_TYPE, INTERNAL_QUADS);
+
     return {
       binary: false,
       data: streamifyArray(quads),
-      metadata: {
-        raw: rawMetadata,
-        dateTime: stats.mtime,
-        contentType: INTERNAL_QUADS,
-      },
+      metadata,
     };
   }
 
diff --git a/src/storage/InMemoryResourceStore.ts b/src/storage/InMemoryResourceStore.ts
index 80b749b25..599b14379 100644
--- a/src/storage/InMemoryResourceStore.ts
+++ b/src/storage/InMemoryResourceStore.ts
@@ -2,9 +2,11 @@ import { PassThrough } from 'stream';
 import arrayifyStream from 'arrayify-stream';
 import streamifyArray from 'streamify-array';
 import { Representation } from '../ldp/representation/Representation';
+import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata';
 import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
 import { TEXT_TURTLE } from '../util/ContentTypes';
 import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
+import { MA_CONTENT_TYPE } from '../util/MetadataTypes';
 import { ensureTrailingSlash } from '../util/Util';
 import { ResourceStore } from './ResourceStore';
 
@@ -24,12 +26,13 @@ export class InMemoryResourceStore implements ResourceStore {
   public constructor(base: string) {
     this.base = ensureTrailingSlash(base);
 
+    const metadata = new RepresentationMetadata({ [MA_CONTENT_TYPE]: TEXT_TURTLE });
     this.store = {
       // Default root entry (what you get when the identifier is equal to the base)
       '': {
         binary: true,
         data: streamifyArray([]),
-        metadata: { raw: [], profiles: [], contentType: TEXT_TURTLE },
+        metadata,
       },
     };
   }
diff --git a/src/storage/RepresentationConvertingStore.ts b/src/storage/RepresentationConvertingStore.ts
index 5d6cb87ad..65ab2b583 100644
--- a/src/storage/RepresentationConvertingStore.ts
+++ b/src/storage/RepresentationConvertingStore.ts
@@ -37,11 +37,12 @@ export class RepresentationConvertingStore<T extends ResourceStore = ResourceSto
     if (!preferences.type) {
       return true;
     }
+    const { contentType } = representation.metadata;
     return Boolean(
-      representation.metadata.contentType &&
+      contentType &&
       preferences.type.some((type): boolean =>
         type.weight > 0 &&
-        matchingMediaType(type.value, representation.metadata.contentType!)),
+        matchingMediaType(type.value, contentType)),
     );
   }
 }
diff --git a/src/storage/conversion/ChainedConverter.ts b/src/storage/conversion/ChainedConverter.ts
index f6683fd85..d0e0bd7d6 100644
--- a/src/storage/conversion/ChainedConverter.ts
+++ b/src/storage/conversion/ChainedConverter.ts
@@ -1,5 +1,7 @@
 import { Representation } from '../../ldp/representation/Representation';
+import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
 import { RepresentationPreferences } from '../../ldp/representation/RepresentationPreferences';
+import { MA_CONTENT_TYPE } from '../../util/MetadataTypes';
 import { matchingMediaType } from '../../util/Util';
 import { RepresentationConverterArgs } from './RepresentationConverter';
 import { TypedRepresentationConverter } from './TypedRepresentationConverter';
@@ -50,8 +52,9 @@ export class ChainedConverter extends TypedRepresentationConverter {
     // Check if the last converter can produce the output
     const idx = this.converters.length - 1;
     const lastChain = await this.getMatchingType(this.converters[idx - 1], this.converters[idx]);
-    const representation: Representation = { ...input.representation };
-    representation.metadata = { ...input.representation.metadata, contentType: lastChain };
+    const oldMeta = input.representation.metadata;
+    const metadata = new RepresentationMetadata(oldMeta, { [MA_CONTENT_TYPE]: lastChain });
+    const representation: Representation = { ...input.representation, metadata };
     await this.last.canHandle({ ...input, representation });
   }
 
diff --git a/src/storage/conversion/QuadToRdfConverter.ts b/src/storage/conversion/QuadToRdfConverter.ts
index 13501663d..0d064a793 100644
--- a/src/storage/conversion/QuadToRdfConverter.ts
+++ b/src/storage/conversion/QuadToRdfConverter.ts
@@ -4,6 +4,7 @@ import { Representation } from '../../ldp/representation/Representation';
 import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
 import { RepresentationPreferences } from '../../ldp/representation/RepresentationPreferences';
 import { INTERNAL_QUADS } from '../../util/ContentTypes';
+import { MA_CONTENT_TYPE } from '../../util/MetadataTypes';
 import { checkRequest, matchingTypes } from './ConversionUtil';
 import { RepresentationConverterArgs } from './RepresentationConverter';
 import { TypedRepresentationConverter } from './TypedRepresentationConverter';
@@ -30,7 +31,7 @@ export class QuadToRdfConverter extends TypedRepresentationConverter {
 
   private async quadsToRdf(quads: Representation, preferences: RepresentationPreferences): Promise<Representation> {
     const contentType = matchingTypes(preferences, await rdfSerializer.getContentTypes())[0].value;
-    const metadata: RepresentationMetadata = { ...quads.metadata, contentType };
+    const metadata = new RepresentationMetadata(quads.metadata, { [MA_CONTENT_TYPE]: contentType });
     return {
       binary: true,
       data: rdfSerializer.serialize(quads.data, { contentType }) as Readable,
diff --git a/src/storage/conversion/QuadToTurtleConverter.ts b/src/storage/conversion/QuadToTurtleConverter.ts
index 460f9a90b..718b4ece3 100644
--- a/src/storage/conversion/QuadToTurtleConverter.ts
+++ b/src/storage/conversion/QuadToTurtleConverter.ts
@@ -2,6 +2,7 @@ import { StreamWriter } from 'n3';
 import { Representation } from '../../ldp/representation/Representation';
 import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
 import { INTERNAL_QUADS, TEXT_TURTLE } from '../../util/ContentTypes';
+import { MA_CONTENT_TYPE } from '../../util/MetadataTypes';
 import { checkRequest } from './ConversionUtil';
 import { RepresentationConverter, RepresentationConverterArgs } from './RepresentationConverter';
 
@@ -18,7 +19,7 @@ export class QuadToTurtleConverter extends RepresentationConverter {
   }
 
   private quadsToTurtle(quads: Representation): Representation {
-    const metadata: RepresentationMetadata = { ...quads.metadata, contentType: TEXT_TURTLE };
+    const metadata = new RepresentationMetadata(quads.metadata, { [MA_CONTENT_TYPE]: TEXT_TURTLE });
     return {
       binary: true,
       data: quads.data.pipe(new StreamWriter({ format: TEXT_TURTLE })),
diff --git a/src/storage/conversion/RdfToQuadConverter.ts b/src/storage/conversion/RdfToQuadConverter.ts
index 9a369ba5f..4a751605c 100644
--- a/src/storage/conversion/RdfToQuadConverter.ts
+++ b/src/storage/conversion/RdfToQuadConverter.ts
@@ -3,6 +3,7 @@ import rdfParser from 'rdf-parse';
 import { Representation } from '../../ldp/representation/Representation';
 import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
 import { INTERNAL_QUADS } from '../../util/ContentTypes';
+import { MA_CONTENT_TYPE } from '../../util/MetadataTypes';
 import { pipeStreamsAndErrors } from '../../util/Util';
 import { checkRequest } from './ConversionUtil';
 import { RepresentationConverterArgs } from './RepresentationConverter';
@@ -29,9 +30,9 @@ export class RdfToQuadConverter extends TypedRepresentationConverter {
   }
 
   private rdfToQuads(representation: Representation, baseIRI: string): Representation {
-    const metadata: RepresentationMetadata = { ...representation.metadata, contentType: INTERNAL_QUADS };
+    const metadata = new RepresentationMetadata(representation.metadata, { [MA_CONTENT_TYPE]: INTERNAL_QUADS });
     const rawQuads = rdfParser.parse(representation.data, {
-      contentType: representation.metadata.contentType as string,
+      contentType: representation.metadata.contentType!,
       baseIRI,
     });
 
diff --git a/src/storage/conversion/TurtleToQuadConverter.ts b/src/storage/conversion/TurtleToQuadConverter.ts
index d584cd542..83c25baa4 100644
--- a/src/storage/conversion/TurtleToQuadConverter.ts
+++ b/src/storage/conversion/TurtleToQuadConverter.ts
@@ -4,6 +4,7 @@ import { Representation } from '../../ldp/representation/Representation';
 import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
 import { TEXT_TURTLE, INTERNAL_QUADS } from '../../util/ContentTypes';
 import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
+import { MA_CONTENT_TYPE } from '../../util/MetadataTypes';
 import { checkRequest } from './ConversionUtil';
 import { RepresentationConverter, RepresentationConverterArgs } from './RepresentationConverter';
 
@@ -20,7 +21,7 @@ export class TurtleToQuadConverter extends RepresentationConverter {
   }
 
   private turtleToQuads(turtle: Representation, baseIRI: string): Representation {
-    const metadata: RepresentationMetadata = { ...turtle.metadata, contentType: INTERNAL_QUADS };
+    const metadata = new RepresentationMetadata(turtle.metadata, { [MA_CONTENT_TYPE]: INTERNAL_QUADS });
 
     // Catch parsing errors and emit correct error
     // Node 10 requires both writableObjectMode and readableObjectMode
diff --git a/src/storage/patch/SparqlUpdatePatchHandler.ts b/src/storage/patch/SparqlUpdatePatchHandler.ts
index b860b309a..98cf9516c 100644
--- a/src/storage/patch/SparqlUpdatePatchHandler.ts
+++ b/src/storage/patch/SparqlUpdatePatchHandler.ts
@@ -6,9 +6,11 @@ import { someTerms } from 'rdf-terms';
 import { Algebra } from 'sparqlalgebrajs';
 import { SparqlUpdatePatch } from '../../ldp/http/SparqlUpdatePatch';
 import { Representation } from '../../ldp/representation/Representation';
+import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
 import { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
 import { INTERNAL_QUADS } from '../../util/ContentTypes';
 import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
+import { MA_CONTENT_TYPE } from '../../util/MetadataTypes';
 import { ResourceLocker } from '../ResourceLocker';
 import { ResourceStore } from '../ResourceStore';
 import { PatchHandler } from './PatchHandler';
@@ -65,14 +67,11 @@ export class SparqlUpdatePatchHandler extends PatchHandler {
     });
     store.removeQuads(deletes);
     store.addQuads(inserts);
+    const metadata = new RepresentationMetadata(input.identifier.path, { [MA_CONTENT_TYPE]: INTERNAL_QUADS });
     const representation: Representation = {
       binary: false,
       data: store.match() as Readable,
-      metadata: {
-        raw: [],
-        profiles: [],
-        contentType: INTERNAL_QUADS,
-      },
+      metadata,
     };
     await this.source.setRepresentation(input.identifier, representation);
 
diff --git a/src/util/InteractionController.ts b/src/util/InteractionController.ts
index 185b0bddd..cd154ad8a 100644
--- a/src/util/InteractionController.ts
+++ b/src/util/InteractionController.ts
@@ -1,3 +1,4 @@
+import type { Term } from 'rdf-js';
 import { v4 as uuid } from 'uuid';
 import { LINK_TYPE_LDP_BC, LINK_TYPE_LDPC } from './LinkTypes';
 import { trimTrailingSlashes } from './Util';
@@ -5,20 +6,20 @@ import { trimTrailingSlashes } from './Util';
 export class InteractionController {
   /**
    * Check whether a new container or a resource should be created based on the given parameters.
-   * @param slug - Incoming slug header.
-   * @param link - Incoming link header.
+   * @param slug - Incoming slug metadata.
+   * @param types - Incoming type metadata.
    */
-  public isContainer(slug?: string, link?: Set<string>): boolean {
-    if (!slug || !slug.endsWith('/')) {
-      return Boolean(link?.has(LINK_TYPE_LDPC)) || Boolean(link?.has(LINK_TYPE_LDP_BC));
+  public isContainer(slug?: string, types?: Term[]): boolean {
+    if (types && types.length > 0) {
+      return types.some((type): boolean => type.value === LINK_TYPE_LDPC || type.value === LINK_TYPE_LDP_BC);
     }
-    return !link || link.has(LINK_TYPE_LDPC) || link.has(LINK_TYPE_LDP_BC);
+    return Boolean(slug?.endsWith('/'));
   }
 
   /**
    * Get the identifier path the new resource should have.
    * @param isContainer - Whether or not the resource is a container.
-   * @param slug - Incoming slug header.
+   * @param slug - Incoming slug metadata.
    */
   public generateIdentifier(isContainer: boolean, slug?: string): string {
     if (!slug) {
diff --git a/src/util/MetadataController.ts b/src/util/MetadataController.ts
index c5d2d0d05..cccb13fdc 100644
--- a/src/util/MetadataController.ts
+++ b/src/util/MetadataController.ts
@@ -4,7 +4,7 @@ import arrayifyStream from 'arrayify-stream';
 import { DataFactory, StreamParser, StreamWriter } from 'n3';
 import type { NamedNode, Quad } from 'rdf-js';
 import streamifyArray from 'streamify-array';
-import { TEXT_TURTLE } from '../util/ContentTypes';
+import { TEXT_TURTLE } from './ContentTypes';
 import { LDP, RDF, STAT, TERMS, XML } from './Prefixes';
 import { pipeStreamsAndErrors } from './Util';
 
diff --git a/src/util/MetadataTypes.ts b/src/util/MetadataTypes.ts
new file mode 100644
index 000000000..2ff7fbc0f
--- /dev/null
+++ b/src/util/MetadataTypes.ts
@@ -0,0 +1,11 @@
+export const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
+
+export const ACL_RESOURCE = 'urn:solid:acl:resource';
+
+export const MA_CONTENT_TYPE = 'http://www.w3.org/ns/ma-ont#format';
+export const HTTP_SLUG = 'urn:solid:http:slug';
+export const HTTP_LAST_CHANGED = 'urn:solid:http:lastChanged';
+export const HTTP_BYTE_SIZE = 'urn:solid:http:byteSize';
+
+export const XSD_DATE_TIME = 'http://www.w3.org/2001/XMLSchema#dateTime';
+export const XSD_INTEGER = 'http://www.w3.org/2001/XMLSchema#integer';
diff --git a/tsdoc.json b/tsdoc.json
new file mode 100644
index 000000000..d7ca66faa
--- /dev/null
+++ b/tsdoc.json
@@ -0,0 +1,9 @@
+{
+  "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
+  "tagDefinitions": [
+    {
+      "tagName": "@ignored",
+      "syntaxKind": "modifier"
+    }
+  ]
+}


Test patch:
diff --git a/test/integration/RepresentationConverter.test.ts b/test/integration/RepresentationConverter.test.ts
index ea664bf8c..9b342ff6b 100644
--- a/test/integration/RepresentationConverter.test.ts
+++ b/test/integration/RepresentationConverter.test.ts
@@ -1,8 +1,10 @@
 import streamifyArray from 'streamify-array';
 import { Representation } from '../../src/ldp/representation/Representation';
+import { RepresentationMetadata } from '../../src/ldp/representation/RepresentationMetadata';
 import { ChainedConverter } from '../../src/storage/conversion/ChainedConverter';
 import { QuadToRdfConverter } from '../../src/storage/conversion/QuadToRdfConverter';
 import { RdfToQuadConverter } from '../../src/storage/conversion/RdfToQuadConverter';
+import { MA_CONTENT_TYPE } from '../../src/util/MetadataTypes';
 import { readableToString } from '../../src/util/Util';
 
 describe('A ChainedConverter', (): void => {
@@ -13,10 +15,11 @@ describe('A ChainedConverter', (): void => {
   const converter = new ChainedConverter(converters);
 
   it('can convert from JSON-LD to turtle.', async(): Promise<void> => {
+    const metadata = new RepresentationMetadata({ [MA_CONTENT_TYPE]: 'application/ld+json' });
     const representation: Representation = {
       binary: true,
       data: streamifyArray([ '{"@id": "http://test.com/s", "http://test.com/p": { "@id": "http://test.com/o" }}' ]),
-      metadata: { raw: [], contentType: 'application/ld+json' },
+      metadata,
     };
 
     const result = await converter.handleSafe({
@@ -30,10 +33,11 @@ describe('A ChainedConverter', (): void => {
   });
 
   it('can convert from turtle to JSON-LD.', async(): Promise<void> => {
+    const metadata = new RepresentationMetadata({ [MA_CONTENT_TYPE]: 'text/turtle' });
     const representation: Representation = {
       binary: true,
       data: streamifyArray([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]),
-      metadata: { raw: [], contentType: 'text/turtle' },
+      metadata,
     };
 
     const result = await converter.handleSafe({
diff --git a/test/integration/RequestParser.test.ts b/test/integration/RequestParser.test.ts
index e9897e30d..4598698e0 100644
--- a/test/integration/RequestParser.test.ts
+++ b/test/integration/RequestParser.test.ts
@@ -5,6 +5,7 @@ import { AcceptPreferenceParser } from '../../src/ldp/http/AcceptPreferenceParse
 import { BasicRequestParser } from '../../src/ldp/http/BasicRequestParser';
 import { BasicTargetExtractor } from '../../src/ldp/http/BasicTargetExtractor';
 import { RawBodyParser } from '../../src/ldp/http/RawBodyParser';
+import { RepresentationMetadata } from '../../src/ldp/representation/RepresentationMetadata';
 import { HttpRequest } from '../../src/server/HttpRequest';
 
 describe('A BasicRequestParser with simple input parsers', (): void => {
@@ -36,12 +37,10 @@ describe('A BasicRequestParser with simple input parsers', (): void => {
       body: {
         data: expect.any(Readable),
         binary: true,
-        metadata: {
-          contentType: 'text/turtle',
-          raw: [],
-        },
+        metadata: expect.any(RepresentationMetadata),
       },
     });
+    expect(result.body?.metadata.contentType).toEqual('text/turtle');
 
     await expect(arrayifyStream(result.body!.data)).resolves.toEqual(
       [ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ],
diff --git a/test/unit/init/Setup.test.ts b/test/unit/init/Setup.test.ts
index e5b9335db..041ca0a21 100644
--- a/test/unit/init/Setup.test.ts
+++ b/test/unit/init/Setup.test.ts
@@ -1,4 +1,5 @@
 import { Setup } from '../../../src/init/Setup';
+import { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier';
 
 describe('Setup', (): void => {
   let httpServer: any;
@@ -10,7 +11,7 @@ describe('Setup', (): void => {
       setRepresentation: jest.fn(async(): Promise<void> => undefined),
     };
     aclManager = {
-      getAcl: jest.fn(async(): Promise<void> => undefined),
+      getAcl: jest.fn(async(): Promise<ResourceIdentifier> => ({ path: 'http://test.com/.acl' })),
     };
     httpServer = {
       listen: jest.fn(),
diff --git a/test/unit/ldp/http/BasicResponseWriter.test.ts b/test/unit/ldp/http/BasicResponseWriter.test.ts
index 1c316c254..a584a4ae6 100644
--- a/test/unit/ldp/http/BasicResponseWriter.test.ts
+++ b/test/unit/ldp/http/BasicResponseWriter.test.ts
@@ -1,10 +1,11 @@
 import { EventEmitter } from 'events';
 import { createResponse, MockResponse } from 'node-mocks-http';
-import type { Quad } from 'rdf-js';
 import streamifyArray from 'streamify-array';
 import { BasicResponseWriter } from '../../../../src/ldp/http/BasicResponseWriter';
 import { ResponseDescription } from '../../../../src/ldp/operations/ResponseDescription';
+import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
 import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError';
+import { MA_CONTENT_TYPE } from '../../../../src/util/MetadataTypes';
 
 describe('A BasicResponseWriter', (): void => {
   const writer = new BasicResponseWriter();
@@ -32,10 +33,7 @@ describe('A BasicResponseWriter', (): void => {
     const body = {
       binary: true,
       data: streamifyArray([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]),
-      metadata: {
-        raw: [] as Quad[],
-        profiles: [] as string[],
-      },
+      metadata: new RepresentationMetadata(),
     };
 
     response.on('end', (): void => {
@@ -50,14 +48,11 @@ describe('A BasicResponseWriter', (): void => {
   });
 
   it('responds with a content-type if the metadata has it.', async(done): Promise<void> => {
+    const metadata = new RepresentationMetadata({ [MA_CONTENT_TYPE]: 'text/turtle' });
     const body = {
       binary: true,
       data: streamifyArray([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]),
-      metadata: {
-        raw: [] as Quad[],
-        profiles: [] as string[],
-        contentType: 'text/turtle',
-      },
+      metadata,
     };
 
     response.on('end', (): void => {
diff --git a/test/unit/ldp/http/RawBodyParser.test.ts b/test/unit/ldp/http/RawBodyParser.test.ts
index e911bea35..3bad403cc 100644
--- a/test/unit/ldp/http/RawBodyParser.test.ts
+++ b/test/unit/ldp/http/RawBodyParser.test.ts
@@ -1,9 +1,11 @@
 import arrayifyStream from 'arrayify-stream';
 import streamifyArray from 'streamify-array';
 import { RawBodyParser } from '../../../../src/ldp/http/RawBodyParser';
+import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
 import { HttpRequest } from '../../../../src/server/HttpRequest';
 import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError';
 import 'jest-rdf';
+import { HTTP_SLUG, RDF_TYPE } from '../../../../src/util/MetadataTypes';
 
 describe('A RawBodyparser', (): void => {
   const bodyParser = new RawBodyParser();
@@ -39,11 +41,9 @@ describe('A RawBodyparser', (): void => {
     expect(result).toEqual({
       binary: true,
       data: input,
-      metadata: {
-        contentType: 'text/turtle',
-        raw: [],
-      },
+      metadata: expect.any(RepresentationMetadata),
     });
+    expect(result.metadata.contentType).toEqual('text/turtle');
     await expect(arrayifyStream(result.data)).resolves.toEqual(
       [ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ],
     );
@@ -53,11 +53,8 @@ describe('A RawBodyparser', (): void => {
     const input = {} as HttpRequest;
     input.headers = { 'transfer-encoding': 'chunked', 'content-type': 'text/turtle', slug: 'slugText' };
     const result = (await bodyParser.handle(input))!;
-    expect(result.metadata).toEqual({
-      contentType: 'text/turtle',
-      raw: [],
-      slug: 'slugText',
-    });
+    expect(result.metadata.contentType).toEqual('text/turtle');
+    expect(result.metadata.get(HTTP_SLUG)?.value).toEqual('slugText');
   });
 
   it('errors if there are multiple slugs.', async(): Promise<void> => {
@@ -68,33 +65,23 @@ describe('A RawBodyparser', (): void => {
     await expect(bodyParser.handle(input)).rejects.toThrow(UnsupportedHttpError);
   });
 
-  it('adds the link headers to the metadata.', async(): Promise<void> => {
+  it('adds the link type headers to the metadata.', async(): Promise<void> => {
     const input = {} as HttpRequest;
     input.headers = { 'transfer-encoding': 'chunked',
       'content-type': 'text/turtle',
       link: '<http://www.w3.org/ns/ldp#Container>; rel="type"' };
     const result = (await bodyParser.handle(input))!;
-    expect(result.metadata).toEqual({
-      contentType: 'text/turtle',
-      raw: [],
-      linkRel: { type: new Set([ 'http://www.w3.org/ns/ldp#Container' ]) },
-    });
+    expect(result.metadata.contentType).toEqual('text/turtle');
+    expect(result.metadata.get(RDF_TYPE)?.value).toEqual('http://www.w3.org/ns/ldp#Container');
   });
 
-  it('supports multiple link headers.', async(): Promise<void> => {
+  it('ignores unknown link headers.', async(): Promise<void> => {
     const input = {} as HttpRequest;
     input.headers = { 'transfer-encoding': 'chunked',
       'content-type': 'text/turtle',
-      link: [ '<http://www.w3.org/ns/ldp#Container>; rel="type"',
-        '<http://www.w3.org/ns/ldp#Resource>; rel="type"',
-        '<unrelatedLink>',
-        'badLink',
-      ]};
+      link: [ '<unrelatedLink>', 'badLink' ]};
     const result = (await bodyParser.handle(input))!;
-    expect(result.metadata).toEqual({
-      contentType: 'text/turtle',
-      raw: [],
-      linkRel: { type: new Set([ 'http://www.w3.org/ns/ldp#Container', 'http://www.w3.org/ns/ldp#Resource' ]) },
-    });
+    expect(result.metadata.quads()).toHaveLength(1);
+    expect(result.metadata.contentType).toEqual('text/turtle');
   });
 });
diff --git a/test/unit/ldp/http/SparqlUpdateBodyParser.test.ts b/test/unit/ldp/http/SparqlUpdateBodyParser.test.ts
index ffee298f1..d325011ae 100644
--- a/test/unit/ldp/http/SparqlUpdateBodyParser.test.ts
+++ b/test/unit/ldp/http/SparqlUpdateBodyParser.test.ts
@@ -34,11 +34,7 @@ describe('A SparqlUpdateBodyParser', (): void => {
       namedNode('http://test.com/o'),
     ) ]);
     expect(result.binary).toBe(true);
-    expect(result.metadata).toEqual({
-      raw: [],
-      profiles: [],
-      contentType: 'application/sparql-update',
-    });
+    expect(result.metadata.contentType).toEqual('application/sparql-update');
 
     // Workaround for Node 10 not exposing objectMode
     expect((await arrayifyStream(result.data)).join('')).toEqual(
diff --git a/test/unit/ldp/representation/RepresentationMetadata.test.ts b/test/unit/ldp/representation/RepresentationMetadata.test.ts
new file mode 100644
index 000000000..fb1422007
--- /dev/null
+++ b/test/unit/ldp/representation/RepresentationMetadata.test.ts
@@ -0,0 +1,195 @@
+import { literal, namedNode, quad } from '@rdfjs/data-model';
+import type { Literal, NamedNode, Quad } from 'rdf-js';
+import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
+import { MA_CONTENT_TYPE } from '../../../../src/util/MetadataTypes';
+
+describe('A RepresentationMetadata', (): void => {
+  let metadata: RepresentationMetadata;
+  const identifier = namedNode('http://example.com/id');
+  const inputQuads = [
+    quad(identifier, namedNode('has'), literal('data')),
+    quad(identifier, namedNode('has'), literal('moreData')),
+    quad(identifier, namedNode('hasOne'), literal('otherData')),
+    quad(namedNode('otherNode'), namedNode('linksTo'), identifier),
+    quad(namedNode('otherNode'), namedNode('has'), literal('otherData')),
+  ];
+
+  describe('constructor', (): void => {
+    it('creates a blank node if no identifier was given.', async(): Promise<void> => {
+      metadata = new RepresentationMetadata();
+      expect(metadata.identifier.termType).toEqual('BlankNode');
+      expect(metadata.quads()).toHaveLength(0);
+    });
+
+    it('stores the given identifier if given.', async(): Promise<void> => {
+      metadata = new RepresentationMetadata(namedNode('identifier'));
+      expect(metadata.identifier).toEqualRdfTerm(namedNode('identifier'));
+    });
+
+    it('converts identifier strings to named nodes.', async(): Promise<void> => {
+      metadata = new RepresentationMetadata('identifier');
+      expect(metadata.identifier).toEqualRdfTerm(namedNode('identifier'));
+    });
+
+    it('copies an other metadata object.', async(): Promise<void> => {
+      const other = new RepresentationMetadata('otherId', { 'test:pred': 'objVal' });
+      metadata = new RepresentationMetadata(other);
+      expect(metadata.identifier).toEqualRdfTerm(namedNode('otherId'));
+      expect(metadata.quads()).toBeRdfIsomorphic([
+        quad(namedNode('otherId'), namedNode('test:pred'), literal('objVal')) ]);
+    });
+
+    it('takes overrides for specific predicates.', async(): Promise<void> => {
+      metadata = new RepresentationMetadata({ predVal: 'objVal' });
+      expect(metadata.get('predVal')).toEqualRdfTerm(literal('objVal'));
+
+      metadata = new RepresentationMetadata({ predVal: literal('objVal') });
+      expect(metadata.get('predVal')).toEqualRdfTerm(literal('objVal'));
+
+      metadata = new RepresentationMetadata({ predVal: [ 'objVal1', literal('objVal2') ], predVal2: 'objVal3' });
+      expect(metadata.getAll('predVal')).toEqualRdfTermArray([ literal('objVal1'), literal('objVal2') ]);
+      expect(metadata.get('predVal2')).toEqualRdfTerm(literal('objVal3'));
+    });
+
+    it('can combine overrides with an identifier.', async(): Promise<void> => {
+      metadata = new RepresentationMetadata(identifier, { predVal: 'objVal' });
+      expect(metadata.quads()).toBeRdfIsomorphic([
+        quad(identifier, namedNode('predVal'), literal('objVal')) ]);
+    });
+
+    it('can combine overrides with other metadata.', async(): Promise<void> => {
+      const other = new RepresentationMetadata('otherId', { 'test:pred': 'objVal' });
+      metadata = new RepresentationMetadata(other, { 'test:pred': 'objVal2' });
+      expect(metadata.quads()).toBeRdfIsomorphic([
+        quad(namedNode('otherId'), namedNode('test:pred'), literal('objVal2')) ]);
+    });
+  });
+
+  describe('instantiated', (): void => {
+    beforeEach(async(): Promise<void> => {
+      metadata = new RepresentationMetadata(identifier).addQuads(inputQuads);
+    });
+
+    it('can get all quads.', async(): Promise<void> => {
+      expect(metadata.quads()).toBeRdfIsomorphic(inputQuads);
+    });
+
+    it('can change the stored identifier.', async(): Promise<void> => {
+      const newIdentifier = namedNode('newNode');
+      metadata.identifier = newIdentifier;
+      const newQuads = inputQuads.map((triple): Quad => {
+        if (triple.subject.equals(identifier)) {
+          return quad(newIdentifier, triple.predicate, triple.object);
+        }
+        if (triple.object.equals(identifier)) {
+          return quad(triple.subject, triple.predicate, newIdentifier);
+        }
+        return triple;
+      });
+      expect(metadata.identifier).toEqualRdfTerm(newIdentifier);
+      expect(metadata.quads()).toBeRdfIsomorphic(newQuads);
+    });
+
+    it('can copy metadata.', async(): Promise<void> => {
+      const other = new RepresentationMetadata(identifier, { 'test:pred': 'objVal' });
+      metadata.setMetadata(other);
+
+      expect(metadata.identifier).toEqual(other.identifier);
+      expect(metadata.quads()).toBeRdfIsomorphic(inputQuads.concat([
+        quad(identifier, namedNode('test:pred'), literal('objVal')) ]));
+    });
+
+    it('updates its identifier when copying metadata.', async(): Promise<void> => {
+      const other = new RepresentationMetadata('otherId', { 'test:pred': 'objVal' });
+      metadata.setMetadata(other);
+
+      // `setMetadata` should have the same result as the following
+      const expectedMetadata = new RepresentationMetadata(identifier).addQuads(inputQuads);
+      expectedMetadata.identifier = namedNode('otherId');
+      expectedMetadata.add('test:pred', 'objVal');
+
+      expect(metadata.identifier).toEqual(other.identifier);
+      expect(metadata.quads()).toBeRdfIsomorphic(expectedMetadata.quads());
+    });
+
+    it('can add quads.', async(): Promise<void> => {
+      const newQuads: Quad[] = [
+        quad(namedNode('random'), namedNode('new'), namedNode('triple')),
+      ];
+      metadata.addQuads(newQuads);
+      expect(metadata.quads()).toBeRdfIsomorphic(newQuads.concat(inputQuads));
+    });
+
+    it('can remove quads.', async(): Promise<void> => {
+      metadata.removeQuads([ inputQuads[0] ]);
+      expect(metadata.quads()).toBeRdfIsomorphic(inputQuads.slice(1));
+    });
+
+    it('can add a single value for a predicate.', async(): Promise<void> => {
+      const newQuad = quad(identifier, namedNode('new'), namedNode('triple'));
+      metadata.add(newQuad.predicate as NamedNode, newQuad.object as NamedNode);
+      expect(metadata.quads()).toBeRdfIsomorphic([ newQuad ].concat(inputQuads));
+    });
+
+    it('can add single values as string.', async(): Promise<void> => {
+      const newQuad = quad(identifier, namedNode('new'), literal('triple'));
+      metadata.add(newQuad.predicate as NamedNode, newQuad.object.value);
+      expect(metadata.quads()).toBeRdfIsomorphic([ newQuad ].concat(inputQuads));
+    });
+
+    it('can remove a single value for a predicate.', async(): Promise<void> => {
+      metadata.remove(inputQuads[0].predicate as NamedNode, inputQuads[0].object as Literal);
+      expect(metadata.quads()).toBeRdfIsomorphic(inputQuads.slice(1));
+    });
+
+    it('can remove single values as string.', async(): Promise<void> => {
+      metadata.remove(inputQuads[0].predicate as NamedNode, inputQuads[0].object.value);
+      expect(metadata.quads()).toBeRdfIsomorphic(inputQuads.slice(1));
+    });
+
+    it('can remove all values for a predicate.', async(): Promise<void> => {
+      const pred = namedNode('has');
+      metadata.removeAll(pred);
+      const updatedNodes = inputQuads.filter((triple): boolean =>
+        !triple.subject.equals(identifier) || !triple.predicate.equals(pred));
+      expect(metadata.quads()).toBeRdfIsomorphic(updatedNodes);
+    });
+
+    it('can get all values for a predicate.', async(): Promise<void> => {
+      expect(metadata.getAll(namedNode('has'))).toEqualRdfTermArray([ literal('data'), literal('moreData') ]);
+    });
+
+    it('can get the single value for a predicate.', async(): Promise<void> => {
+      expect(metadata.get(namedNode('hasOne'))).toEqualRdfTerm(literal('otherData'));
+    });
+
+    it('returns undefined if getting an undefined predicate.', async(): Promise<void> => {
+      expect(metadata.get(namedNode('doesntExist'))).toBeUndefined();
+    });
+
+    it('errors if there are multiple values when getting a value.', async(): Promise<void> => {
+      expect((): any => metadata.get(namedNode('has'))).toThrow(Error);
+      expect((): any => metadata.get('has')).toThrow(Error);
+    });
+
+    it('can set the value of a predicate.', async(): Promise<void> => {
+      metadata.set(namedNode('has'), literal('singleValue'));
+      expect(metadata.get(namedNode('has'))).toEqualRdfTerm(literal('singleValue'));
+    });
+
+    it('has a shorthand for content-type.', async(): Promise<void> => {
+      expect(metadata.contentType).toBeUndefined();
+      metadata.contentType = 'a/b';
+      expect(metadata.get(MA_CONTENT_TYPE)).toEqualRdfTerm(literal('a/b'));
+      expect(metadata.contentType).toEqual('a/b');
+      metadata.contentType = undefined;
+      expect(metadata.contentType).toBeUndefined();
+    });
+
+    it('errors if a shorthand has multiple values.', async(): Promise<void> => {
+      metadata.add(MA_CONTENT_TYPE, 'a/b');
+      metadata.add(MA_CONTENT_TYPE, 'c/d');
+      expect((): any => metadata.contentType).toThrow();
+    });
+  });
+});
diff --git a/test/unit/storage/FileResourceStore.test.ts b/test/unit/storage/FileResourceStore.test.ts
index ca8e7c6ca..972a5e8f7 100644
--- a/test/unit/storage/FileResourceStore.test.ts
+++ b/test/unit/storage/FileResourceStore.test.ts
@@ -1,7 +1,7 @@
 import fs, { promises as fsPromises, Stats, WriteStream } from 'fs';
 import { posix } from 'path';
 import { Readable } from 'stream';
-import { literal, namedNode, quad as quadRDF, triple } from '@rdfjs/data-model';
+import { literal, namedNode, quad as quadRDF } from '@rdfjs/data-model';
 import arrayifyStream from 'arrayify-stream';
 import { DataFactory } from 'n3';
 import streamifyArray from 'streamify-array';
@@ -17,6 +17,7 @@ import { UnsupportedMediaTypeHttpError } from '../../../src/util/errors/Unsuppor
 import { InteractionController } from '../../../src/util/InteractionController';
 import { LINK_TYPE_LDP_BC, LINK_TYPE_LDPR } from '../../../src/util/LinkTypes';
 import { MetadataController } from '../../../src/util/MetadataController';
+import { HTTP_BYTE_SIZE, HTTP_LAST_CHANGED, HTTP_SLUG, RDF_TYPE } from '../../../src/util/MetadataTypes';
 import { LDP, RDF, STAT, TERMS, XML } from '../../../src/util/Prefixes';
 
 const { join: joinPath } = posix;
@@ -44,11 +45,6 @@ describe('A FileResourceStore', (): void => {
   let stats: Stats;
   let writeStream: WriteStream;
   const rawData = 'lorem ipsum dolor sit amet consectetur adipiscing';
-  const quad = triple(
-    namedNode('http://test.com/s'),
-    namedNode('http://test.com/p'),
-    namedNode('http://test.com/o'),
-  );
 
   beforeEach(async(): Promise<void> => {
     jest.clearAllMocks();
@@ -62,13 +58,14 @@ describe('A FileResourceStore', (): void => {
     representation = {
       binary: true,
       data: streamifyArray([ rawData ]),
-      metadata: { raw: [], linkRel: { type: new Set() }} as RepresentationMetadata,
+      metadata: new RepresentationMetadata(),
     };
 
     stats = {
       isDirectory: jest.fn((): any => false) as Function,
       isFile: jest.fn((): any => false) as Function,
       mtime: new Date(),
+      size: 5,
     } as jest.Mocked<Stats>;
 
     writeStream = {
@@ -136,7 +133,8 @@ describe('A FileResourceStore', (): void => {
     (fs.createReadStream as jest.Mock).mockImplementationOnce((): any => new Error('Metadata file does not exist.'));
 
     // Write container (POST)
-    representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDP_BC ]) }, slug: 'myContainer/', raw: []};
+    representation.metadata.add(RDF_TYPE, LINK_TYPE_LDP_BC);
+    representation.metadata.add(HTTP_SLUG, 'myContainer/');
     const identifier = await store.addResource({ path: base }, representation);
     expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'myContainer/'), { recursive: true });
     expect(identifier.path).toBe(`${base}myContainer/`);
@@ -146,12 +144,10 @@ describe('A FileResourceStore', (): void => {
     expect(result).toEqual({
       binary: false,
       data: expect.any(Readable),
-      metadata: {
-        raw: [],
-        dateTime: stats.mtime,
-        contentType: INTERNAL_QUADS,
-      },
+      metadata: expect.any(RepresentationMetadata),
     });
+    expect(result.metadata.get(HTTP_LAST_CHANGED)?.value).toEqual(stats.mtime.toISOString());
+    expect(result.metadata.contentType).toEqual(INTERNAL_QUADS);
     await expect(arrayifyStream(result.data)).resolves.toBeDefined();
   });
 
@@ -160,7 +156,8 @@ describe('A FileResourceStore', (): void => {
     (fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
 
     // Tests
-    representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDP_BC ]) }, slug: 'myContainer/', raw: []};
+    representation.metadata.add(RDF_TYPE, LINK_TYPE_LDP_BC);
+    representation.metadata.add(HTTP_SLUG, 'myContainer/');
     await expect(store.addResource({ path: `${base}foo` }, representation)).rejects.toThrow(MethodNotAllowedHttpError);
     expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo'));
   });
@@ -176,17 +173,19 @@ describe('A FileResourceStore', (): void => {
     (fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
 
     // Tests
-    representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDP_BC ]) }, slug: 'myContainer/', raw: []};
+    representation.metadata.add(RDF_TYPE, LINK_TYPE_LDP_BC);
+    representation.metadata.add(HTTP_SLUG, 'myContainer/');
     await expect(store.addResource({ path: `${base}doesnotexist` }, representation))
       .rejects.toThrow(MethodNotAllowedHttpError);
     expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'doesnotexist'));
 
-    representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDPR ]) }, slug: 'file.txt', raw: []};
+    representation.metadata.set(RDF_TYPE, LINK_TYPE_LDPR);
+    representation.metadata.set(HTTP_SLUG, 'file.txt');
     await expect(store.addResource({ path: `${base}doesnotexist` }, representation))
       .rejects.toThrow(MethodNotAllowedHttpError);
     expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'doesnotexist'));
 
-    representation.metadata = { linkRel: { type: new Set() }, slug: 'file.txt', raw: []};
+    representation.metadata.removeAll(RDF_TYPE);
     await expect(store.addResource({ path: `${base}existingresource` }, representation))
       .rejects.toThrow(MethodNotAllowedHttpError);
     expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'existingresource'));
@@ -216,13 +215,11 @@ describe('A FileResourceStore', (): void => {
     expect(result).toEqual({
       binary: true,
       data: expect.any(Readable),
-      metadata: {
-        raw: [],
-        dateTime: stats.mtime,
-        byteSize: stats.size,
-        contentType: 'text/plain',
-      },
+      metadata: expect.any(RepresentationMetadata),
     });
+    expect(result.metadata.get(HTTP_LAST_CHANGED)?.value).toEqual(stats.mtime.toISOString());
+    expect(result.metadata.get(HTTP_BYTE_SIZE)?.value).toEqual(`${stats.size}`);
+    expect(result.metadata.contentType).toEqual('text/plain');
     await expect(arrayifyStream(result.data)).resolves.toEqual([ rawData ]);
     expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt'));
     expect(fs.createReadStream as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt'));
@@ -257,7 +254,8 @@ describe('A FileResourceStore', (): void => {
     (fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
 
     // Tests
-    representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDPR ]) }, slug: 'file.txt', raw: []};
+    representation.metadata.add(RDF_TYPE, LINK_TYPE_LDPR);
+    representation.metadata.add(HTTP_SLUG, 'file.txt');
     const identifier = await store.addResource({ path: `${base}doesnotexistyet/` }, representation);
     expect(identifier.path).toBe(`${base}doesnotexistyet/file.txt`);
     expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'doesnotexistyet/'),
@@ -282,13 +280,12 @@ describe('A FileResourceStore', (): void => {
     (fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
 
     // Tests
-    representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDPR ]) }, raw: [ quad ]};
+    representation.metadata.add(RDF_TYPE, LINK_TYPE_LDPR);
     representation.data = readableMock;
     await store.addResource({ path: `${base}foo/` }, representation);
     expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo/'), { recursive: true });
     expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo/'));
 
-    representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDPR ]) }, raw: [ quad ]};
     await store.setRepresentation({ path: `${base}foo/file.txt` }, representation);
     expect(fs.createWriteStream as jest.Mock).toBeCalledTimes(4);
   });
@@ -369,12 +366,10 @@ describe('A FileResourceStore', (): void => {
     expect(result).toEqual({
       binary: false,
       data: expect.any(Readable),
-      metadata: {
-        raw: [],
-        dateTime: stats.mtime,
-        contentType: INTERNAL_QUADS,
-      },
+      metadata: expect.any(RepresentationMetadata),
     });
+    expect(result.metadata.get(HTTP_LAST_CHANGED)?.value).toEqual(stats.mtime.toISOString());
+    expect(result.metadata.contentType).toEqual(INTERNAL_QUADS);
     await expect(arrayifyStream(result.data)).resolves.toEqualRdfQuadArray(quads);
     expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo/'));
     expect(fsPromises.readdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo/'));
@@ -382,7 +377,7 @@ describe('A FileResourceStore', (): void => {
     expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo', '.nonresource'));
   });
 
-  it('can overwrite representation with PUT.', async(): Promise<void> => {
+  it('can overwrite representation and its metadata with PUT.', async(): Promise<void> => {
     // Mock the fs functions.
     stats.isFile = jest.fn((): any => true);
     (fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
@@ -392,9 +387,9 @@ describe('A FileResourceStore', (): void => {
     (fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
 
     // Tests
-    representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDPR ]) }, raw: []};
+    representation.metadata.add(RDF_TYPE, LINK_TYPE_LDPR);
     await store.setRepresentation({ path: `${base}alreadyexists.txt` }, representation);
-    expect(fs.createWriteStream as jest.Mock).toBeCalledTimes(1);
+    expect(fs.createWriteStream as jest.Mock).toBeCalledTimes(2);
     expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'alreadyexists.txt'));
     expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(rootFilepath, { recursive: true });
   });
@@ -408,7 +403,7 @@ describe('A FileResourceStore', (): void => {
     await expect(store.setRepresentation({ path: `${base}alreadyexists` }, representation)).rejects
       .toThrow(ConflictHttpError);
     expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'alreadyexists'));
-    representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDP_BC ]) }, raw: []};
+    representation.metadata.add(RDF_TYPE, LINK_TYPE_LDP_BC);
     await expect(store.setRepresentation({ path: `${base}alreadyexists/` }, representation)).rejects
       .toThrow(ConflictHttpError);
     expect(fsPromises.access as jest.Mock).toBeCalledTimes(1);
@@ -422,7 +417,7 @@ describe('A FileResourceStore', (): void => {
     (fsPromises.mkdir as jest.Mock).mockReturnValueOnce(true);
 
     // Tests
-    representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDP_BC ]) }, raw: []};
+    representation.metadata.add(RDF_TYPE, LINK_TYPE_LDP_BC);
     await store.setRepresentation({ path: `${base}foo/` }, representation);
     expect(fsPromises.mkdir as jest.Mock).toBeCalledTimes(1);
     expect(fsPromises.access as jest.Mock).toBeCalledTimes(1);
@@ -440,7 +435,8 @@ describe('A FileResourceStore', (): void => {
     (fsPromises.unlink as jest.Mock).mockReturnValueOnce(true);
 
     // Tests
-    representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDPR ]) }, slug: 'file.txt', raw: [ quad ]};
+    representation.metadata.add(RDF_TYPE, LINK_TYPE_LDPR);
+    representation.metadata.add(HTTP_SLUG, 'file.txt');
     await expect(store.addResource({ path: base }, representation)).rejects.toThrow(Error);
     expect(fs.createWriteStream as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt.metadata'));
     expect(fs.createWriteStream as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt'));
@@ -456,7 +452,8 @@ describe('A FileResourceStore', (): void => {
     (fsPromises.rmdir as jest.Mock).mockReturnValueOnce(true);
 
     // Tests
-    representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDP_BC ]) }, slug: 'foo/', raw: [ quad ]};
+    representation.metadata.add(RDF_TYPE, LINK_TYPE_LDP_BC);
+    representation.metadata.add(HTTP_SLUG, 'foo/');
     await expect(store.addResource({ path: base }, representation)).rejects.toThrow(Error);
     expect(fsPromises.rmdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo/'));
   });
@@ -467,7 +464,7 @@ describe('A FileResourceStore', (): void => {
     (fsPromises.mkdir as jest.Mock).mockReturnValueOnce(true);
 
     // Tests
-    representation.metadata = { slug: 'myContainer/', raw: []};
+    representation.metadata.add(HTTP_SLUG, 'myContainer/');
     const identifier = await store.addResource({ path: base }, representation);
     expect(identifier.path).toBe(`${base}myContainer/`);
     expect(fsPromises.mkdir as jest.Mock).toBeCalledTimes(1);
@@ -485,13 +482,11 @@ describe('A FileResourceStore', (): void => {
     expect(result).toEqual({
       binary: true,
       data: expect.any(Readable),
-      metadata: {
-        raw: [],
-        contentType: 'application/octet-stream',
-        dateTime: stats.mtime,
-        byteSize: stats.size,
-      },
+      metadata: expect.any(RepresentationMetadata),
     });
+    expect(result.metadata.contentType).toEqual('application/octet-stream');
+    expect(result.metadata.get(HTTP_LAST_CHANGED)?.value).toEqual(stats.mtime.toISOString());
+    expect(result.metadata.get(HTTP_BYTE_SIZE)?.value).toEqual(`${stats.size}`);
   });
 
   it('errors when performing a PUT on the root path.', async(): Promise<void> => {
@@ -510,7 +505,6 @@ describe('A FileResourceStore', (): void => {
     (fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
 
     // Tests
-    representation.metadata = { raw: []};
     await store.setRepresentation({ path: `${base}file.txt` }, representation);
     expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(rootFilepath, { recursive: true });
     expect(fs.createWriteStream as jest.Mock).toBeCalledTimes(1);
@@ -525,10 +519,44 @@ describe('A FileResourceStore', (): void => {
       (fsPromises.mkdir as jest.Mock).mockReturnValue(true);
 
       // Tests
-      representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDP_BC ]) }, slug: 'bar', raw: []};
+      representation.metadata.add(RDF_TYPE, LINK_TYPE_LDP_BC);
+      representation.metadata.add(HTTP_SLUG, 'bar');
       const identifier = await store.addResource({ path: `${base}foo` }, representation);
       expect(identifier.path).toBe(`${base}foo/bar/`);
       expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo'));
       expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo', 'bar/'), { recursive: false });
     });
+
+  it('generates a new URI when adding without a slug.', async(): Promise<void> => {
+    // Mock the fs functions.
+    // Post
+    (fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
+    (fsPromises.mkdir as jest.Mock).mockReturnValue(true);
+    stats.isDirectory = jest.fn((): any => true);
+    (fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
+
+    // Mock: Get
+    stats.isFile = jest.fn((): any => true);
+    (fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
+    (fs.createReadStream as jest.Mock).mockReturnValueOnce(streamifyArray([ rawData ]));
+    (fs.createReadStream as jest.Mock).mockImplementationOnce((): any => new Error('Metadata file does not exist.'));
+
+    // Tests
+    await store.addResource({ path: base }, representation);
+    const filePath: string = (fs.createWriteStream as jest.Mock).mock.calls[0][0];
+    expect(filePath.startsWith(rootFilepath)).toBeTruthy();
+    const name = filePath.slice(rootFilepath.length);
+    const result = await store.getRepresentation({ path: `${base}${name}` });
+    expect(result).toEqual({
+      binary: true,
+      data: expect.any(Readable),
+      metadata: expect.any(RepresentationMetadata),
+    });
+    expect(result.metadata.get(HTTP_LAST_CHANGED)?.value).toEqual(stats.mtime.toISOString());
+    expect(result.metadata.get(HTTP_BYTE_SIZE)?.value).toEqual(`${stats.size}`);
+    await expect(arrayifyStream(result.data)).resolves.toEqual([ rawData ]);
+    expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, name));
+    expect(fs.createReadStream as jest.Mock).toBeCalledWith(joinPath(rootFilepath, name));
+    expect(fs.createReadStream as jest.Mock).toBeCalledWith(joinPath(rootFilepath, `${name}.metadata`));
+  });
 });
diff --git a/test/unit/storage/RepresentationConvertingStore.test.ts b/test/unit/storage/RepresentationConvertingStore.test.ts
index 4d6fd32c7..321942791 100644
--- a/test/unit/storage/RepresentationConvertingStore.test.ts
+++ b/test/unit/storage/RepresentationConvertingStore.test.ts
@@ -1,16 +1,19 @@
+import { RepresentationMetadata } from '../../../src/ldp/representation/RepresentationMetadata';
 import { RepresentationConverter } from '../../../src/storage/conversion/RepresentationConverter';
 import { RepresentationConvertingStore } from '../../../src/storage/RepresentationConvertingStore';
 import { ResourceStore } from '../../../src/storage/ResourceStore';
+import { MA_CONTENT_TYPE } from '../../../src/util/MetadataTypes';
 
 describe('A RepresentationConvertingStore', (): void => {
   let store: RepresentationConvertingStore;
   let source: ResourceStore;
   let handleSafeFn: jest.Mock<Promise<void>, []>;
   let converter: RepresentationConverter;
+  const metadata = new RepresentationMetadata({ [MA_CONTENT_TYPE]: 'text/turtle' });
 
   beforeEach(async(): Promise<void> => {
     source = {
-      getRepresentation: jest.fn(async(): Promise<any> => ({ data: 'data', metadata: { contentType: 'text/turtle' }})),
+      getRepresentation: jest.fn(async(): Promise<any> => ({ data: 'data', metadata })),
     } as unknown as ResourceStore;
 
     handleSafeFn = jest.fn(async(): Promise<any> => 'converter');
@@ -20,12 +23,14 @@ describe('A RepresentationConvertingStore', (): void => {
   });
 
   it('returns the Representation from the source if no changes are required.', async(): Promise<void> => {
-    await expect(store.getRepresentation({ path: 'path' }, { type: [
+    const result = await store.getRepresentation({ path: 'path' }, { type: [
       { value: 'text/*', weight: 0 }, { value: 'text/turtle', weight: 1 },
-    ]})).resolves.toEqual({
+    ]});
+    expect(result).toEqual({
       data: 'data',
-      metadata: { contentType: 'text/turtle' },
+      metadata: expect.any(RepresentationMetadata),
     });
+    expect(result.metadata.contentType).toEqual('text/turtle');
     expect(source.getRepresentation).toHaveBeenCalledTimes(1);
     expect(source.getRepresentation).toHaveBeenLastCalledWith(
       { path: 'path' }, { type: [{ value: 'text/*', weight: 0 }, { value: 'text/turtle', weight: 1 }]}, undefined,
@@ -34,10 +39,12 @@ describe('A RepresentationConvertingStore', (): void => {
   });
 
   it('returns the Representation from the source if there are no preferences.', async(): Promise<void> => {
-    await expect(store.getRepresentation({ path: 'path' }, {})).resolves.toEqual({
+    const result = await store.getRepresentation({ path: 'path' }, {});
+    expect(result).toEqual({
       data: 'data',
-      metadata: { contentType: 'text/turtle' },
+      metadata: expect.any(RepresentationMetadata),
     });
+    expect(result.metadata.contentType).toEqual('text/turtle');
     expect(source.getRepresentation).toHaveBeenCalledTimes(1);
     expect(source.getRepresentation).toHaveBeenLastCalledWith(
       { path: 'path' }, {}, undefined,
@@ -53,7 +60,7 @@ describe('A RepresentationConvertingStore', (): void => {
     expect(handleSafeFn).toHaveBeenCalledTimes(1);
     expect(handleSafeFn).toHaveBeenLastCalledWith({
       identifier: { path: 'path' },
-      representation: { data: 'data', metadata: { contentType: 'text/turtle' }},
+      representation: { data: 'data', metadata },
       preferences: { type: [{ value: 'text/plain', weight: 1 }, { value: 'text/turtle', weight: 0 }]},
     });
   });
diff --git a/test/unit/storage/conversion/ChainedConverter.test.ts b/test/unit/storage/conversion/ChainedConverter.test.ts
index d010a0c6b..5f8d2956b 100644
--- a/test/unit/storage/conversion/ChainedConverter.test.ts
+++ b/test/unit/storage/conversion/ChainedConverter.test.ts
@@ -1,9 +1,11 @@
 import { Representation } from '../../../../src/ldp/representation/Representation';
+import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
 import { RepresentationPreferences } from '../../../../src/ldp/representation/RepresentationPreferences';
 import { ChainedConverter } from '../../../../src/storage/conversion/ChainedConverter';
 import { checkRequest } from '../../../../src/storage/conversion/ConversionUtil';
 import { RepresentationConverterArgs } from '../../../../src/storage/conversion/RepresentationConverter';
 import { TypedRepresentationConverter } from '../../../../src/storage/conversion/TypedRepresentationConverter';
+import { MA_CONTENT_TYPE } from '../../../../src/util/MetadataTypes';
 
 class DummyConverter extends TypedRepresentationConverter {
   private readonly inTypes: { [contentType: string]: number };
@@ -28,9 +30,9 @@ class DummyConverter extends TypedRepresentationConverter {
   }
 
   public async handle(input: RepresentationConverterArgs): Promise<Representation> {
-    const representation: Representation = { ...input.representation };
-    representation.metadata = { ...input.representation.metadata, contentType: input.preferences.type![0].value };
-    return representation;
+    const metadata = new RepresentationMetadata(input.representation.metadata,
+      { [MA_CONTENT_TYPE]: input.preferences.type![0].value });
+    return { ...input.representation, metadata };
   }
 }
 
@@ -49,7 +51,8 @@ describe('A ChainedConverter', (): void => {
     ];
     converter = new ChainedConverter(converters);
 
-    representation = { metadata: { contentType: 'text/turtle' } as any } as Representation;
+    const metadata = new RepresentationMetadata({ [MA_CONTENT_TYPE]: 'text/turtle' });
+    representation = { metadata } as Representation;
     preferences = { type: [{ value: 'internal/quads', weight: 1 }]};
     args = { representation, preferences, identifier: { path: 'path' }};
   });
diff --git a/test/unit/storage/conversion/ConversionUtil.test.ts b/test/unit/storage/conversion/ConversionUtil.test.ts
index a3f0e5c25..c46ba2af3 100644
--- a/test/unit/storage/conversion/ConversionUtil.test.ts
+++ b/test/unit/storage/conversion/ConversionUtil.test.ts
@@ -1,35 +1,42 @@
 import { Representation } from '../../../../src/ldp/representation/Representation';
+import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
 import { RepresentationPreferences } from '../../../../src/ldp/representation/RepresentationPreferences';
 import { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceIdentifier';
 import { checkRequest, matchingTypes } from '../../../../src/storage/conversion/ConversionUtil';
 
 describe('A ConversionUtil', (): void => {
   const identifier: ResourceIdentifier = { path: 'path' };
+  let representation: Representation;
+  let metadata: RepresentationMetadata;
+
+  beforeEach(async(): Promise<void> => {
+    metadata = new RepresentationMetadata();
+    representation = { metadata } as Representation;
+  });
 
   describe('#checkRequest', (): void => {
     it('requires an input type.', async(): Promise<void> => {
-      const representation = { metadata: {}} as Representation;
       const preferences: RepresentationPreferences = {};
       expect((): any => checkRequest({ identifier, representation, preferences }, [ '*/*' ], [ '*/*' ]))
         .toThrow('Input type required for conversion.');
     });
 
     it('requires a matching input type.', async(): Promise<void> => {
-      const representation = { metadata: { contentType: 'a/x' }} as Representation;
+      metadata.contentType = 'a/x';
       const preferences: RepresentationPreferences = { type: [{ value: 'b/x', weight: 1 }]};
       expect((): any => checkRequest({ identifier, representation, preferences }, [ 'c/x' ], [ '*/*' ]))
         .toThrow('Can only convert from c/x to */*.');
     });
 
     it('requires a matching output type.', async(): Promise<void> => {
-      const representation = { metadata: { contentType: 'a/x' }} as Representation;
+      metadata.contentType = 'a/x';
       const preferences: RepresentationPreferences = { type: [{ value: 'b/x', weight: 1 }]};
       expect((): any => checkRequest({ identifier, representation, preferences }, [ '*/*' ], [ 'c/x' ]))
         .toThrow('Can only convert from */* to c/x.');
     });
 
     it('succeeds with a valid input and output type.', async(): Promise<void> => {
-      const representation = { metadata: { contentType: 'a/x' }} as Representation;
+      metadata.contentType = 'a/x';
       const preferences: RepresentationPreferences = { type: [{ value: 'b/x', weight: 1 }]};
       expect(checkRequest({ identifier, representation, preferences }, [ '*/*' ], [ '*/*' ]))
         .toBeUndefined();
diff --git a/test/unit/storage/conversion/QuadToRdfConverter.test.ts b/test/unit/storage/conversion/QuadToRdfConverter.test.ts
index 987832c94..d9d0c3f5e 100644
--- a/test/unit/storage/conversion/QuadToRdfConverter.test.ts
+++ b/test/unit/storage/conversion/QuadToRdfConverter.test.ts
@@ -3,14 +3,17 @@ import rdfSerializer from 'rdf-serialize';
 import stringifyStream from 'stream-to-string';
 import streamifyArray from 'streamify-array';
 import { Representation } from '../../../../src/ldp/representation/Representation';
+import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
 import { RepresentationPreferences } from '../../../../src/ldp/representation/RepresentationPreferences';
 import { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceIdentifier';
 import { QuadToRdfConverter } from '../../../../src/storage/conversion/QuadToRdfConverter';
 import { INTERNAL_QUADS } from '../../../../src/util/ContentTypes';
+import { MA_CONTENT_TYPE } from '../../../../src/util/MetadataTypes';
 
 describe('A QuadToRdfConverter', (): void => {
   const converter = new QuadToRdfConverter();
   const identifier: ResourceIdentifier = { path: 'path' };
+  const metadata = new RepresentationMetadata({ [MA_CONTENT_TYPE]: INTERNAL_QUADS });
 
   it('supports parsing quads.', async(): Promise<void> => {
     await expect(converter.getInputTypes()).resolves.toEqual({ [INTERNAL_QUADS]: 1 });
@@ -21,13 +24,13 @@ describe('A QuadToRdfConverter', (): void => {
   });
 
   it('can handle quad to turtle conversions.', async(): Promise<void> => {
-    const representation = { metadata: { contentType: INTERNAL_QUADS }} as Representation;
+    const representation = { metadata } as Representation;
     const preferences: RepresentationPreferences = { type: [{ value: 'text/turtle', weight: 1 }]};
     await expect(converter.canHandle({ identifier, representation, preferences })).resolves.toBeUndefined();
   });
 
   it('can handle quad to JSON-LD conversions.', async(): Promise<void> => {
-    const representation = { metadata: { contentType: INTERNAL_QUADS }} as Representation;
+    const representation = { metadata } as Representation;
     const preferences: RepresentationPreferences = { type: [{ value: 'application/ld+json', weight: 1 }]};
     await expect(converter.canHandle({ identifier, representation, preferences })).resolves.toBeUndefined();
   });
@@ -39,16 +42,15 @@ describe('A QuadToRdfConverter', (): void => {
         namedNode('http://test.com/p'),
         namedNode('http://test.com/o'),
       ) ]),
-      metadata: { contentType: INTERNAL_QUADS },
+      metadata,
     } as Representation;
     const preferences: RepresentationPreferences = { type: [{ value: 'text/turtle', weight: 1 }]};
     const result = await converter.handle({ identifier, representation, preferences });
     expect(result).toMatchObject({
       binary: true,
-      metadata: {
-        contentType: 'text/turtle',
-      },
+      metadata: expect.any(RepresentationMetadata),
     });
+    expect(result.metadata.contentType).toEqual('text/turtle');
     await expect(stringifyStream(result.data)).resolves.toEqual(
       `<http://test.com/s> <http://test.com/p> <http://test.com/o>.
 `,
@@ -56,22 +58,22 @@ describe('A QuadToRdfConverter', (): void => {
   });
 
   it('converts quads to JSON-LD.', async(): Promise<void> => {
+    metadata.contentType = INTERNAL_QUADS;
     const representation = {
       data: streamifyArray([ triple(
         namedNode('http://test.com/s'),
         namedNode('http://test.com/p'),
         namedNode('http://test.com/o'),
       ) ]),
-      metadata: { contentType: INTERNAL_QUADS },
+      metadata,
     } as Representation;
     const preferences: RepresentationPreferences = { type: [{ value: 'application/ld+json', weight: 1 }]};
     const result = await converter.handle({ identifier, representation, preferences });
     expect(result).toMatchObject({
       binary: true,
-      metadata: {
-        contentType: 'application/ld+json',
-      },
+      metadata: expect.any(RepresentationMetadata),
     });
+    expect(result.metadata.contentType).toEqual('application/ld+json');
     await expect(stringifyStream(result.data)).resolves.toEqual(
       `[
   {
diff --git a/test/unit/storage/conversion/QuadToTurtleConverter.test.ts b/test/unit/storage/conversion/QuadToTurtleConverter.test.ts
index 218f4bdda..fe70504b8 100644
--- a/test/unit/storage/conversion/QuadToTurtleConverter.test.ts
+++ b/test/unit/storage/conversion/QuadToTurtleConverter.test.ts
@@ -2,17 +2,20 @@ import { namedNode, triple } from '@rdfjs/data-model';
 import arrayifyStream from 'arrayify-stream';
 import streamifyArray from 'streamify-array';
 import { Representation } from '../../../../src/ldp/representation/Representation';
+import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
 import { RepresentationPreferences } from '../../../../src/ldp/representation/RepresentationPreferences';
 import { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceIdentifier';
 import { QuadToTurtleConverter } from '../../../../src/storage/conversion/QuadToTurtleConverter';
 import { INTERNAL_QUADS } from '../../../../src/util/ContentTypes';
+import { MA_CONTENT_TYPE } from '../../../../src/util/MetadataTypes';
 
 describe('A QuadToTurtleConverter', (): void => {
   const converter = new QuadToTurtleConverter();
   const identifier: ResourceIdentifier = { path: 'path' };
+  const metadata = new RepresentationMetadata({ [MA_CONTENT_TYPE]: INTERNAL_QUADS });
 
   it('can handle quad to turtle conversions.', async(): Promise<void> => {
-    const representation = { metadata: { contentType: INTERNAL_QUADS }} as Representation;
+    const representation = { metadata } as Representation;
     const preferences: RepresentationPreferences = { type: [{ value: 'text/turtle', weight: 1 }]};
     await expect(converter.canHandle({ identifier, representation, preferences })).resolves.toBeUndefined();
   });
@@ -24,16 +27,15 @@ describe('A QuadToTurtleConverter', (): void => {
         namedNode('http://test.com/p'),
         namedNode('http://test.com/o'),
       ) ]),
-      metadata: { contentType: INTERNAL_QUADS },
+      metadata,
     } as Representation;
     const preferences: RepresentationPreferences = { type: [{ value: 'text/turtle', weight: 1 }]};
     const result = await converter.handle({ identifier, representation, preferences });
     expect(result).toMatchObject({
       binary: true,
-      metadata: {
-        contentType: 'text/turtle',
-      },
+      metadata: expect.any(RepresentationMetadata),
     });
+    expect(result.metadata.contentType).toEqual('text/turtle');
     await expect(arrayifyStream(result.data)).resolves.toContain(
       '<http://test.com/s> <http://test.com/p> <http://test.com/o>',
     );
diff --git a/test/unit/storage/conversion/RdfToQuadConverter.test.ts b/test/unit/storage/conversion/RdfToQuadConverter.test.ts
index 08158c397..c842b0f07 100644
--- a/test/unit/storage/conversion/RdfToQuadConverter.test.ts
+++ b/test/unit/storage/conversion/RdfToQuadConverter.test.ts
@@ -4,11 +4,13 @@ import arrayifyStream from 'arrayify-stream';
 import rdfParser from 'rdf-parse';
 import streamifyArray from 'streamify-array';
 import { Representation } from '../../../../src/ldp/representation/Representation';
+import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
 import { RepresentationPreferences } from '../../../../src/ldp/representation/RepresentationPreferences';
 import { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceIdentifier';
 import { RdfToQuadConverter } from '../../../../src/storage/conversion/RdfToQuadConverter';
 import { INTERNAL_QUADS } from '../../../../src/util/ContentTypes';
 import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError';
+import { MA_CONTENT_TYPE } from '../../../../src/util/MetadataTypes';
 
 describe('A RdfToQuadConverter.test.ts', (): void => {
   const converter = new RdfToQuadConverter();
@@ -23,31 +25,33 @@ describe('A RdfToQuadConverter.test.ts', (): void => {
   });
 
   it('can handle turtle to quad conversions.', async(): Promise<void> => {
-    const representation = { metadata: { contentType: 'text/turtle' }} as Representation;
+    const metadata = new RepresentationMetadata({ [MA_CONTENT_TYPE]: 'text/turtle' });
+    const representation = { metadata } as Representation;
     const preferences: RepresentationPreferences = { type: [{ value: INTERNAL_QUADS, weight: 1 }]};
     await expect(converter.canHandle({ identifier, representation, preferences })).resolves.toBeUndefined();
   });
 
   it('can handle JSON-LD to quad conversions.', async(): Promise<void> => {
-    const representation = { metadata: { contentType: 'application/ld+json' }} as Representation;
+    const metadata = new RepresentationMetadata({ [MA_CONTENT_TYPE]: 'application/ld+json' });
+    const representation = { metadata } as Representation;
     const preferences: RepresentationPreferences = { type: [{ value: INTERNAL_QUADS, weight: 1 }]};
     await expect(converter.canHandle({ identifier, representation, preferences })).resolves.toBeUndefined();
   });
 
   it('converts turtle to quads.', async(): Promise<void> => {
+    const metadata = new RepresentationMetadata({ [MA_CONTENT_TYPE]: 'text/turtle' });
     const representation = {
       data: streamifyArray([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]),
-      metadata: { contentType: 'text/turtle' },
+      metadata,
     } as Representation;
     const preferences: RepresentationPreferences = { type: [{ value: INTERNAL_QUADS, weight: 1 }]};
     const result = await converter.handle({ identifier, representation, preferences });
     expect(result).toEqual({
       binary: false,
       data: expect.any(Readable),
-      metadata: {
-        contentType: INTERNAL_QUADS,
-      },
+      metadata: expect.any(RepresentationMetadata),
     });
+    expect(result.metadata.contentType).toEqual(INTERNAL_QUADS);
     await expect(arrayifyStream(result.data)).resolves.toEqualRdfQuadArray([ triple(
       namedNode('http://test.com/s'),
       namedNode('http://test.com/p'),
@@ -56,19 +60,19 @@ describe('A RdfToQuadConverter.test.ts', (): void => {
   });
 
   it('converts JSON-LD to quads.', async(): Promise<void> => {
+    const metadata = new RepresentationMetadata({ [MA_CONTENT_TYPE]: 'application/ld+json' });
     const representation = {
       data: streamifyArray([ '{"@id": "http://test.com/s", "http://test.com/p": { "@id": "http://test.com/o" }}' ]),
-      metadata: { contentType: 'application/ld+json' },
+      metadata,
     } as Representation;
     const preferences: RepresentationPreferences = { type: [{ value: INTERNAL_QUADS, weight: 1 }]};
     const result = await converter.handle({ identifier, representation, preferences });
     expect(result).toEqual({
       binary: false,
       data: expect.any(Readable),
-      metadata: {
-        contentType: INTERNAL_QUADS,
-      },
+      metadata: expect.any(RepresentationMetadata),
     });
+    expect(result.metadata.contentType).toEqual(INTERNAL_QUADS);
     await expect(arrayifyStream(result.data)).resolves.toEqualRdfQuadArray([ triple(
       namedNode('http://test.com/s'),
       namedNode('http://test.com/p'),
@@ -77,19 +81,19 @@ describe('A RdfToQuadConverter.test.ts', (): void => {
   });
 
   it('throws an UnsupportedHttpError on invalid triple data.', async(): Promise<void> => {
+    const metadata = new RepresentationMetadata({ [MA_CONTENT_TYPE]: 'text/turtle' });
     const representation = {
       data: streamifyArray([ '<http://test.com/s> <http://test.com/p> <http://test.co' ]),
-      metadata: { contentType: 'text/turtle' },
+      metadata,
     } as Representation;
     const preferences: RepresentationPreferences = { type: [{ value: INTERNAL_QUADS, weight: 1 }]};
     const result = await converter.handle({ identifier, representation, preferences });
     expect(result).toEqual({
       binary: false,
       data: expect.any(Readable),
-      metadata: {
-        contentType: INTERNAL_QUADS,
-      },
+      metadata: expect.any(RepresentationMetadata),
     });
+    expect(result.metadata.contentType).toEqual(INTERNAL_QUADS);
     await expect(arrayifyStream(result.data)).rejects.toThrow(UnsupportedHttpError);
   });
 });
diff --git a/test/unit/storage/conversion/TurtleToQuadConverter.test.ts b/test/unit/storage/conversion/TurtleToQuadConverter.test.ts
index f3042d9fc..c708248d5 100644
--- a/test/unit/storage/conversion/TurtleToQuadConverter.test.ts
+++ b/test/unit/storage/conversion/TurtleToQuadConverter.test.ts
@@ -3,18 +3,21 @@ import { namedNode, triple } from '@rdfjs/data-model';
 import arrayifyStream from 'arrayify-stream';
 import streamifyArray from 'streamify-array';
 import { Representation } from '../../../../src/ldp/representation/Representation';
+import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
 import { RepresentationPreferences } from '../../../../src/ldp/representation/RepresentationPreferences';
 import { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceIdentifier';
 import { TurtleToQuadConverter } from '../../../../src/storage/conversion/TurtleToQuadConverter';
 import { INTERNAL_QUADS } from '../../../../src/util/ContentTypes';
 import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError';
+import { MA_CONTENT_TYPE } from '../../../../src/util/MetadataTypes';
 
 describe('A TurtleToQuadConverter', (): void => {
   const converter = new TurtleToQuadConverter();
   const identifier: ResourceIdentifier = { path: 'path' };
+  const metadata = new RepresentationMetadata({ [MA_CONTENT_TYPE]: 'text/turtle' });
 
   it('can handle turtle to quad conversions.', async(): Promise<void> => {
-    const representation = { metadata: { contentType: 'text/turtle' }} as Representation;
+    const representation = { metadata } as Representation;
     const preferences: RepresentationPreferences = { type: [{ value: INTERNAL_QUADS, weight: 1 }]};
     await expect(converter.canHandle({ identifier, representation, preferences })).resolves.toBeUndefined();
   });
@@ -22,17 +25,16 @@ describe('A TurtleToQuadConverter', (): void => {
   it('converts turtle to quads.', async(): Promise<void> => {
     const representation = {
       data: streamifyArray([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]),
-      metadata: { contentType: 'text/turtle' },
+      metadata,
     } as Representation;
     const preferences: RepresentationPreferences = { type: [{ value: INTERNAL_QUADS, weight: 1 }]};
     const result = await converter.handle({ identifier, representation, preferences });
     expect(result).toEqual({
       binary: false,
       data: expect.any(Readable),
-      metadata: {
-        contentType: INTERNAL_QUADS,
-      },
+      metadata: expect.any(RepresentationMetadata),
     });
+    expect(result.metadata.contentType).toEqual(INTERNAL_QUADS);
     await expect(arrayifyStream(result.data)).resolves.toEqualRdfQuadArray([ triple(
       namedNode('http://test.com/s'),
       namedNode('http://test.com/p'),
@@ -43,17 +45,16 @@ describe('A TurtleToQuadConverter', (): void => {
   it('throws an UnsupportedHttpError on invalid triple data.', async(): Promise<void> => {
     const representation = {
       data: streamifyArray([ '<http://test.com/s> <http://test.com/p> <http://test.co' ]),
-      metadata: { contentType: 'text/turtle' },
+      metadata,
     } as Representation;
     const preferences: RepresentationPreferences = { type: [{ value: INTERNAL_QUADS, weight: 1 }]};
     const result = await converter.handle({ identifier, representation, preferences });
     expect(result).toEqual({
       binary: false,
       data: expect.any(Readable),
-      metadata: {
-        contentType: INTERNAL_QUADS,
-      },
+      metadata: expect.any(RepresentationMetadata),
     });
+    expect(result.metadata.contentType).toEqual(INTERNAL_QUADS);
     await expect(arrayifyStream(result.data)).rejects.toThrow(UnsupportedHttpError);
   });
 });
diff --git a/test/unit/storage/patch/SparqlUpdatePatchHandler.test.ts b/test/unit/storage/patch/SparqlUpdatePatchHandler.test.ts
index 394e02a13..8d3d96e24 100644
--- a/test/unit/storage/patch/SparqlUpdatePatchHandler.test.ts
+++ b/test/unit/storage/patch/SparqlUpdatePatchHandler.test.ts
@@ -4,6 +4,7 @@ import type { Quad } from 'rdf-js';
 import { translate } from 'sparqlalgebrajs';
 import streamifyArray from 'streamify-array';
 import { SparqlUpdatePatch } from '../../../../src/ldp/http/SparqlUpdatePatch';
+import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
 import { Lock } from '../../../../src/storage/Lock';
 import { SparqlUpdatePatchHandler } from '../../../../src/storage/patch/SparqlUpdatePatchHandler';
 import { ResourceLocker } from '../../../../src/storage/ResourceLocker';
@@ -73,8 +74,9 @@ describe('A SparqlUpdatePatchHandler', (): void => {
     expect(setParams[0]).toEqual({ path: 'path' });
     expect(setParams[1]).toEqual(expect.objectContaining({
       binary: false,
-      metadata: { raw: [], profiles: [], contentType: INTERNAL_QUADS },
+      metadata: expect.any(RepresentationMetadata),
     }));
+    expect(setParams[1].metadata.contentType).toEqual(INTERNAL_QUADS);
     await expect(arrayifyStream(setParams[1].data)).resolves.toBeRdfIsomorphic(quads);
   };
 
diff --git a/test/util/TestHelpers.ts b/test/util/TestHelpers.ts
index ad86eb690..fc1367609 100644
--- a/test/util/TestHelpers.ts
+++ b/test/util/TestHelpers.ts
@@ -5,10 +5,11 @@ import { join } from 'path';
 import * as url from 'url';
 import { createResponse, MockResponse } from 'node-mocks-http';
 import streamifyArray from 'streamify-array';
-import { ResourceStore } from '../../index';
+import { RepresentationMetadata, ResourceStore } from '../../index';
 import { PermissionSet } from '../../src/ldp/permissions/PermissionSet';
 import { HttpHandler } from '../../src/server/HttpHandler';
 import { HttpRequest } from '../../src/server/HttpRequest';
+import { MA_CONTENT_TYPE } from '../../src/util/MetadataTypes';
 import { call } from './Util';
 
 export class AclTestHelper {
@@ -49,11 +50,7 @@ export class AclTestHelper {
     const representation = {
       binary: true,
       data: streamifyArray(acl),
-      metadata: {
-        raw: [],
-        profiles: [],
-        contentType: 'text/turtle',
-      },
+      metadata: new RepresentationMetadata({ [MA_CONTENT_TYPE]: 'text/turtle' }),
     };
 
     return this.store.setRepresentation(