diff --git a/gplugin/gplugin-loader.h b/gplugin/gplugin-loader.h
--- a/gplugin/gplugin-loader.h
+++ b/gplugin/gplugin-loader.h
@@ -57,6 +57,8 @@
 	gpointer reserved[4];
 };
 
+const gchar *gplugin_loader_get_id(GPluginLoader *loader);
+
 GSList *gplugin_loader_get_supported_extensions(GPluginLoader *loader);
 
 GPluginPlugin *gplugin_loader_query_plugin(
# HG changeset patch
# Node ID 708ec89e4c45
# Parent  52ccb9444f88
diff --git a/gplugin/gplugin-loader.c b/gplugin/gplugin-loader.c
--- a/gplugin/gplugin-loader.c
+++ b/gplugin/gplugin-loader.c
@@ -53,10 +53,92 @@
  * #GPluginLoaderClass defines the behavior for loading plugins.
  */
 
+typedef struct {
+	gchar *id;
+} GPluginLoaderPrivate;
+
+enum {
+	PROP_0,
+	PROP_ID,
+	N_PROPERTIES,
+};
+static GParamSpec *properties[N_PROPERTIES] = {
+	NULL,
+};
+
+G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE(
+	GPluginLoader,
+	gplugin_loader,
+	G_TYPE_OBJECT);
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static void
+gplugin_loader_set_id(GPluginLoader *loader, const gchar *id)
+{
+	GPluginLoaderPrivate *priv = gplugin_loader_get_instance_private(loader);
+
+	g_free(priv->id);
+	priv->id = g_strdup(id);
+
+	g_object_notify_by_pspec(G_OBJECT(loader), properties[PROP_ID]);
+}
+
 /******************************************************************************
  * GObject Implementation
  *****************************************************************************/
-G_DEFINE_ABSTRACT_TYPE(GPluginLoader, gplugin_loader, G_TYPE_OBJECT);
+static void
+gplugin_loader_get_property(
+	GObject *obj,
+	guint param_id,
+	GValue *value,
+	GParamSpec *pspec)
+{
+	GPluginLoader *loader = GPLUGIN_LOADER(obj);
+
+	switch(param_id) {
+		case PROP_ID:
+			g_value_set_string(value, gplugin_loader_get_id(loader));
+			break;
+		default:
+			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+			break;
+	}
+}
+
+static void
+gplugin_loader_set_property(
+	GObject *obj,
+	guint param_id,
+	const GValue *value,
+	GParamSpec *pspec)
+{
+	GPluginLoader *loader = GPLUGIN_LOADER(obj);
+
+	switch(param_id) {
+		case PROP_ID:
+			gplugin_loader_set_id(loader, g_value_get_string(value));
+			break;
+		default:
+			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+			break;
+	}
+}
+
+static void
+gplugin_loader_finalize(GObject *obj)
+{
+	GPluginLoader *loader = NULL;
+	GPluginLoaderPrivate *priv = NULL;
+
+	loader = GPLUGIN_LOADER(obj);
+	priv = gplugin_loader_get_instance_private(loader);
+
+	g_clear_pointer(&priv->id, g_free);
+
+	G_OBJECT_CLASS(gplugin_loader_parent_class)->finalize(obj);
+}
 
 static void
 gplugin_loader_init(G_GNUC_UNUSED GPluginLoader *loader)
@@ -66,6 +148,27 @@
 static void
 gplugin_loader_class_init(G_GNUC_UNUSED GPluginLoaderClass *klass)
 {
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+
+	obj_class->get_property = gplugin_loader_get_property;
+	obj_class->set_property = gplugin_loader_set_property;
+	obj_class->finalize = gplugin_loader_finalize;
+
+	/**
+	 * GPluginLoader::id:
+	 *
+	 * The identifier of the loader.
+	 *
+	 * Since: 0.34.0
+	 */
+	properties[PROP_ID] = g_param_spec_string(
+		"id",
+		"id",
+		"The identifier of the loader",
+		NULL,
+		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
 }
 
 /******************************************************************************
@@ -73,6 +176,28 @@
  *****************************************************************************/
 
 /**
+ * gplugin_loader_get_id:
+ * @loader: The #GPluginLoader instance.
+ *
+ * Gets the identifier of @loader.
+ *
+ * Returns: The ID of @loader.
+ *
+ * Since: 0.34.0
+ */
+const gchar *
+gplugin_loader_get_id(GPluginLoader *loader)
+{
+	GPluginLoaderPrivate *priv = NULL;
+
+	g_return_val_if_fail(GPLUGIN_IS_LOADER(loader), NULL);
+
+	priv = gplugin_loader_get_instance_private(loader);
+
+	return priv->id;
+}
+
+/**
  * gplugin_loader_query_plugin:
  * @loader: The #GPluginLoader instance performing the query.
  * @filename: The filename to query.
diff --git a/gplugin/reference/gplugin-docs.xml b/gplugin/reference/gplugin-docs.xml
--- a/gplugin/reference/gplugin-docs.xml
+++ b/gplugin/reference/gplugin-docs.xml
@@ -79,5 +79,9 @@
     <title>Index of new symbols in 0.33.0</title>
     <xi:include href="xml/api-index-0.33.0.xml"><xi:fallback /></xi:include>
   </index>
+  <index id="api-0.34.0">
+    <title>Index of new symbols in 0.34.0</title>
+    <xi:include href="xml/api-index-0.34.0.xml"><xi:fallback /></xi:include>
+  </index>
   <xi:include href="xml/annotation-glossary.xml"><xi:fallback /></xi:include>
 </book>
diff --git a/gplugin/tests/meson.build b/gplugin/tests/meson.build
--- a/gplugin/tests/meson.build
+++ b/gplugin/tests/meson.build
@@ -38,6 +38,10 @@
 	dependencies : [gplugin_dep, GLIB, GOBJECT])
 test('ID Normalization', e)
 
+e = executable('test-loader', 'test-loader.c',
+	dependencies : [gplugin_dep, GLIB, GOBJECT])
+test('Loader', e)
+
 e = executable('test-loader-registration', 'test-loader-registration.c',
 	dependencies : [gplugin_dep, GLIB, GOBJECT])
 test('Loader Registration', e)
diff --git a/gplugin/tests/test-loader.c b/gplugin/tests/test-loader.c
new file mode 100644
--- /dev/null
+++ b/gplugin/tests/test-loader.c
@@ -0,0 +1,326 @@
+/*
+ * Copyright (C) 2011-2021 Gary Kramlich <grim@reaperworld.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <glib.h>
+
+#include <gplugin.h>
+
+/******************************************************************************
+ * TestGPluginPlugin
+ *****************************************************************************/
+#define TEST_GPLUGIN_TYPE_PLUGIN (test_gplugin_plugin_get_type())
+G_DECLARE_FINAL_TYPE(
+	TestGPluginPlugin,
+	test_gplugin_plugin,
+	TEST_GPLUGIN,
+	PLUGIN,
+	GObject)
+
+enum {
+	PROP_0,
+	PROP_FILENAME,
+	PROP_LOADER,
+	PROP_INFO,
+	PROP_STATE,
+	PROP_ERROR,
+};
+
+struct _TestGPluginPlugin {
+	GObject parent;
+
+	/* overrides */
+	gchar *filename;
+	GPluginLoader *loader;
+	GPluginPluginInfo *info;
+	GPluginPluginState state;
+	GError *error;
+};
+
+static void
+test_gplugin_plugin_iface_init(GPluginPluginInterface *iface)
+{
+}
+
+G_DEFINE_TYPE_WITH_CODE(
+	TestGPluginPlugin,
+	test_gplugin_plugin,
+	G_TYPE_OBJECT,
+	G_IMPLEMENT_INTERFACE(GPLUGIN_TYPE_PLUGIN, test_gplugin_plugin_iface_init))
+
+static void
+test_gplugin_plugin_get_property(
+	GObject *obj,
+	guint param_id,
+	GValue *value,
+	GParamSpec *pspec)
+{
+	TestGPluginPlugin *plugin = TEST_GPLUGIN_PLUGIN(obj);
+	switch(param_id) {
+		case PROP_FILENAME:
+			g_value_set_string(value, plugin->filename);
+			break;
+		case PROP_LOADER:
+			g_value_set_object(value, plugin->loader);
+			break;
+		case PROP_INFO:
+			g_value_set_object(value, plugin->info);
+			break;
+		case PROP_STATE:
+			g_value_set_enum(value, plugin->state);
+			break;
+		case PROP_ERROR:
+			g_value_set_boxed(value, plugin->error);
+			break;
+		default:
+			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+			break;
+	}
+}
+
+static void
+test_gplugin_plugin_set_property(
+	GObject *obj,
+	guint param_id,
+	const GValue *value,
+	GParamSpec *pspec)
+{
+	TestGPluginPlugin *plugin = TEST_GPLUGIN_PLUGIN(obj);
+
+	switch(param_id) {
+		case PROP_FILENAME:
+			plugin->filename = g_strdup(g_value_get_string(value));
+			break;
+		case PROP_LOADER:
+			plugin->loader = g_value_dup_object(value);
+			break;
+		case PROP_INFO:
+			plugin->info = g_value_dup_object(value);
+			break;
+		case PROP_STATE:
+			plugin->state = g_value_get_enum(value);
+			break;
+		case PROP_ERROR:
+			plugin->error = g_value_dup_boxed(value);
+			break;
+		default:
+			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+			break;
+	}
+}
+
+static void
+test_gplugin_plugin_finalize(GObject *obj)
+{
+	TestGPluginPlugin *plugin = TEST_GPLUGIN_PLUGIN(obj);
+
+	g_clear_pointer(&plugin->filename, g_free);
+	g_clear_object(&plugin->loader);
+	g_clear_object(&plugin->info);
+	g_clear_error(&plugin->error);
+
+	G_OBJECT_CLASS(test_gplugin_plugin_parent_class)->finalize(obj);
+}
+
+static void
+test_gplugin_plugin_init(TestGPluginPlugin *plugin)
+{
+}
+
+static void
+test_gplugin_plugin_class_init(TestGPluginPluginClass *klass)
+{
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+
+	obj_class->get_property = test_gplugin_plugin_get_property;
+	obj_class->set_property = test_gplugin_plugin_set_property;
+	obj_class->finalize = test_gplugin_plugin_finalize;
+
+	g_object_class_override_property(obj_class, PROP_FILENAME, "filename");
+	g_object_class_override_property(obj_class, PROP_LOADER, "loader");
+	g_object_class_override_property(obj_class, PROP_INFO, "info");
+	g_object_class_override_property(obj_class, PROP_STATE, "state");
+	g_object_class_override_property(obj_class, PROP_ERROR, "error");
+}
+
+/******************************************************************************
+ * TestGPluginLoader
+ *****************************************************************************/
+#define TEST_GPLUGIN_TYPE_LOADER (test_gplugin_loader_get_type())
+G_DECLARE_FINAL_TYPE(
+	TestGPluginLoader,
+	test_gplugin_loader,
+	TEST_GPLUGIN,
+	LOADER,
+	GPluginLoader)
+
+struct _TestGPluginLoader {
+	GPluginLoader parent;
+
+	gboolean supported_extensions;
+	gboolean query;
+	gboolean load;
+	gboolean unload;
+};
+
+static GSList *
+test_gplugin_loader_supported_extensions(GPluginLoader *l)
+{
+	TestGPluginLoader *loader = TEST_GPLUGIN_LOADER(l);
+
+	loader->supported_extensions = TRUE;
+
+	return g_slist_append(NULL, "");
+}
+
+static GPluginPlugin *
+test_gplugin_loader_query(
+	GPluginLoader *l,
+	G_GNUC_UNUSED const gchar *filename,
+	G_GNUC_UNUSED GError **error)
+{
+	TestGPluginLoader *loader = TEST_GPLUGIN_LOADER(l);
+
+	loader->query = TRUE;
+
+	return GPLUGIN_PLUGIN(g_object_new(TEST_GPLUGIN_TYPE_PLUGIN, NULL));
+}
+
+static gboolean
+test_gplugin_loader_load(
+	GPluginLoader *l,
+	G_GNUC_UNUSED GPluginPlugin *plugin,
+	G_GNUC_UNUSED GError **error)
+{
+	TestGPluginLoader *loader = TEST_GPLUGIN_LOADER(l);
+
+	loader->load = TRUE;
+
+	return TRUE;
+}
+
+static gboolean
+test_gplugin_loader_unload(
+	GPluginLoader *l,
+	G_GNUC_UNUSED GPluginPlugin *plugin,
+	G_GNUC_UNUSED GError **error)
+{
+	TestGPluginLoader *loader = TEST_GPLUGIN_LOADER(l);
+
+	loader->unload = TRUE;
+
+	return TRUE;
+}
+
+G_DEFINE_TYPE(TestGPluginLoader, test_gplugin_loader, GPLUGIN_TYPE_LOADER)
+
+static void
+test_gplugin_loader_init(TestGPluginLoader *loader)
+{
+}
+
+static void
+test_gplugin_loader_class_init(TestGPluginLoaderClass *klass)
+{
+	GPluginLoaderClass *loader_class = GPLUGIN_LOADER_CLASS(klass);
+
+	loader_class->supported_extensions =
+		test_gplugin_loader_supported_extensions;
+	loader_class->query = test_gplugin_loader_query;
+	loader_class->load = test_gplugin_loader_load;
+	loader_class->unload = test_gplugin_loader_unload;
+}
+
+static GPluginLoader *
+test_gplugin_loader_new(void)
+{
+	/* clang-format off */
+	return GPLUGIN_LOADER(g_object_new(
+		TEST_GPLUGIN_TYPE_LOADER,
+		"id", "test-loader",
+		NULL));
+	/* clang-format on */
+}
+/******************************************************************************
+ * Tests
+ *****************************************************************************/
+static void
+test_gplugin_loader_properties(void)
+{
+	GPluginLoader *loader = test_gplugin_loader_new();
+	gchar *id = NULL;
+
+	g_assert_cmpstr("test-loader", ==, gplugin_loader_get_id(loader));
+
+	g_object_get(G_OBJECT(loader), "id", &id, NULL);
+	g_assert_cmpstr("test-loader", ==, id);
+	g_clear_pointer(&id, g_free);
+
+	g_clear_object(&loader);
+}
+
+static void
+test_gplugin_loader_methods(void)
+{
+	GPluginLoader *loader = test_gplugin_loader_new();
+	TestGPluginLoader *tl = TEST_GPLUGIN_LOADER(loader);
+	GPluginPlugin *plugin = NULL;
+	GError *error = NULL;
+	GSList *exts = NULL;
+	gboolean r = FALSE;
+
+	g_assert_false(tl->supported_extensions);
+	g_assert_false(tl->query);
+	g_assert_false(tl->load);
+	g_assert_false(tl->unload);
+
+	exts = gplugin_loader_get_supported_extensions(loader);
+	g_assert_nonnull(exts);
+	g_slist_free(exts);
+	g_assert_true(tl->supported_extensions);
+
+	plugin = gplugin_loader_query_plugin(loader, ":test:", &error);
+	g_assert_no_error(error);
+	g_assert_nonnull(plugin);
+	g_assert_true(tl->query);
+
+	r = gplugin_loader_load_plugin(loader, plugin, &error);
+	g_assert_no_error(error);
+	g_assert_true(r);
+	g_assert_true(tl->load);
+
+	r = gplugin_loader_unload_plugin(loader, plugin, &error);
+	g_assert_no_error(error);
+	g_assert_true(r);
+	g_assert_true(tl->unload);
+
+	g_clear_object(&plugin);
+	g_clear_object(&loader);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+gint
+main(gint argc, gchar **argv)
+{
+	g_test_init(&argc, &argv, NULL);
+
+	g_test_add_func("/loader/properties", test_gplugin_loader_properties);
+	g_test_add_func("/loader/methods", test_gplugin_loader_methods);
+
+	return g_test_run();
+}
diff --git a/meson.build b/meson.build
--- a/meson.build
+++ b/meson.build
@@ -3,7 +3,7 @@
 ###############################################################################
 project('gplugin', 'c',
 	license : 'LGPL-2.0-or-later',
-	version : '0.33.1-dev',
+	version : '0.34.0-dev',
 	meson_version : '>=0.56.0',
 	default_options : ['c_std=c99'])
 
