Lightweight, extensible .NET library for wrapping success responses in consistent API envelopes. Framework-agnostic core with factory pattern for maximum flexibility.
$ dotnet add package SuccessHoundApiResponse<TData, TMeta> - no runtime castingApiResponse<T>dotnet add package SuccessHound
dotnet add package SuccessHound.AspNetExtensionsdotnet add package SuccessHound
dotnet add package SuccessHound.AspNetExtensions
dotnet add package SuccessHound.Paginationusing SuccessHound.Extensions;
using SuccessHound.Defaults;
var builder = WebApplication.CreateBuilder(args);
// Configure SuccessHound with DI
builder.Services.AddSuccessHound(options =>
{
options.UseFormatter<DefaultSuccessFormatter>();
});
var app = builder.Build();
// Optional: Add middleware (currently pass-through)
app.UseSuccessHound();
app.MapGet("/users/{id}", (int id, HttpContext context) =>
{
var user = new { Id = id, Name = "John Doe", Email = "john@example.com" };
return user.Ok(context); // Wraps in success envelope
});
app.Run();Response:
{
"success": true,
"data": {
"id": 1,
"name": "John Doe",
"email": "john@example.com"
},
"meta": null,
"timestamp": "2025-12-15T10:30:00.000Z"
}using SuccessHound.Extensions;
using SuccessHound.Defaults;
using SuccessHound.Pagination;
using SuccessHound.Pagination.Extensions;
var builder = WebApplication.CreateBuilder(args);
// Configure SuccessHound with pagination
builder.Services.AddSuccessHound(options =>
{
options.UseFormatter<DefaultSuccessFormatter>();
options.UsePagination(); // Add this line!
});
var app = builder.Build();
app.UseSuccessHound();
app.MapGet("/users", async (AppDbContext db, HttpContext context, int page = 1, int pageSize = 10) =>
{
return await db.Users
.OrderBy(u => u.Id)
.ToPagedResultAsync(page, pageSize, context);
});
app.Run();Response:
{
"success": true,
"data": [
{
"id": 1,
"name": "Alice"
},
{
"id": 2,
"name": "Bob"
}
],
"meta": {
"pagination": {
"page": 1,
"pageSize": 10,
"totalCount": 100,
"totalPages": 10,
"hasNextPage": true,
"hasPreviousPage": false
}
},
"timestamp": "2025-12-15T10:30:00.000Z"
}SuccessHound uses a strongly-typed response envelope system:
// Two-generic envelope for responses with metadata
public class ApiResponse<TData, TMeta>
{
public bool Success { get; init; } // Always true for success responses
public TData? Data { get; init; } // Your payload
public TMeta? Meta { get; init; } // Strongly-typed metadata
public DateTime Timestamp { get; init; } // UTC timestamp
}
// Backward-compatible wrapper for responses without metadata
public class ApiResponse<T> : ApiResponse<T, NoMeta>
{
// Inherits all properties, Meta is always NoMeta.Instance
}.Ok<T>()Returns 200 OK with wrapped data.
app.MapGet("/products/{id}", (int id, HttpContext context) =>
{
var product = GetProduct(id);
return product.Ok(context);
});Response: 200 OK
{
"success": true,
"data": {
"id": 1,
"name": "Product Name"
},
"meta": null,
"timestamp": "2025-12-15T10:30:00.000Z"
}.Created<T>(string location)Returns 201 Created with Location header.
app.MapPost("/products", (Product product, HttpContext context) =>
{
var created = CreateProduct(product);
return created.Created($"/products/{created.Id}", context);
});Response: 201 Created with Location: /products/123 header
.Updated<T>()Returns 200 OK for update operations.
app.MapPut("/products/{id}", (int id, Product product, HttpContext context) =>
{
var updated = UpdateProduct(id, product);
return updated.Updated(context);
});Response: 200 OK with wrapped data
.NoContent() / .Deleted()Returns 204 No Content for delete operations.
app.MapDelete("/products/{id}", (int id) =>
{
DeleteProduct(id);
return SuccessHoundResultsExtensions.Deleted();
});Response: 204 No Content (no body)
.WithMeta<TData, TMeta>(TMeta meta) (Strongly-Typed)Returns 200 OK with strongly-typed metadata. Recommended for new code.
// Define your metadata type
public class VersionMeta
{
public string Version { get; init; } = "v1.0";
public DateTime ServerTime { get; init; } = DateTime.UtcNow;
}
app.MapGet("/products", (HttpContext context, int page = 1) =>
{
var products = GetProducts(page);
var meta = new VersionMeta { Version = "v2.0" };
return products.WithMeta(meta, context);
});Response:
{
"success": true,
"data": [
...
],
"meta": {
"version": "v2.0",
"serverTime": "2025-12-15T10:30:00.000Z"
},
"timestamp": "2025-12-15T10:30:00.000Z"
}.WithMeta<T>(object meta) (Backward-Compatible)Returns 200 OK with custom metadata. Still supported for backward compatibility.
app.MapGet("/products", (HttpContext context, int page = 1) =>
{
var products = GetProducts(page);
var meta = new
{
Page = page,
Version = "v1.0",
ServerTime = DateTime.UtcNow
};
return products.WithMeta(meta, context);
});.Custom<T>(int statusCode)Returns custom HTTP status code.
app.MapPost("/products/process", (Product product, HttpContext context) =>
{
var result = ProcessProduct(product);
return result.Custom(202, context); // 202 Accepted
});Response: 202 Accepted with wrapped data
using SuccessHound.Pagination.Extensions;
app.MapGet("/users", async (AppDbContext db, HttpContext context, int page = 1, int pageSize = 20) =>
{
return await db.Users
.Where(u => u.IsActive)
.OrderBy(u => u.CreatedAt)
.ToPagedResultAsync(page, pageSize, context);
});app.MapGet("/items", (HttpContext context, int page = 1, int pageSize = 10) =>
{
var items = GetAllItems(); // Returns IEnumerable<T>
return items.ToPagedResult(page, pageSize, context);
});Pagination now uses strongly-typed PaginationMeta:
public class PaginationMeta
{
public int Page { get; init; }
public int PageSize { get; init; }
public int TotalCount { get; init; }
public int TotalPages { get; init; }
public bool HasNextPage { get; init; }
public bool HasPreviousPage { get; init; }
}Response:
{
"success": true,
"data": [
{ "id": 1, "name": "Alice" },
{ "id": 2, "name": "Bob" }
],
"meta": {
"page": 1,
"pageSize": 10,
"totalCount": 100,
"totalPages": 10,
"hasNextPage": true,
"hasPreviousPage": false
},
"timestamp": "2025-12-15T10:30:00.000Z"
}You can also use pagination with explicit types:
using SuccessHound.Defaults;
using SuccessHound.Pagination.Defaults;
app.MapGet("/users", async (AppDbContext db, HttpContext context, int page = 1, int pageSize = 10) =>
{
var users = await db.Users
.OrderBy(u => u.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
var totalCount = await db.Users.CountAsync();
var factory = context.RequestServices.GetRequiredService<IPaginationMetadataFactory>();
var meta = factory.CreateMetadata(page, pageSize, totalCount);
// Explicitly typed response
return Results.Ok(ApiResponse<IReadOnlyList<User>, PaginationMeta>.Ok(users, meta));
});Create your own response structure:
using SuccessHound.Abstractions;
public sealed class MyCustomFormatter : ISuccessResponseFormatter
{
public object Format(object? data, object? meta = null)
{
return new
{
Status = "success",
Result = data,
Metadata = meta,
Version = "v2.0",
Timestamp = DateTime.UtcNow
};
}
}Use it:
builder.Services.AddSuccessHound(options =>
{
options.UseFormatter<MyCustomFormatter>();
});Customize pagination metadata by implementing IPaginationMetadataFactory:
using SuccessHound.Pagination.Abstractions;
using SuccessHound.Pagination.Defaults;
public sealed class MyPaginationFactory : IPaginationMetadataFactory
{
public PaginationMeta CreateMetadata(int page, int pageSize, int totalCount)
{
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
// Return strongly-typed PaginationMeta
return new PaginationMeta
{
Page = page,
PageSize = pageSize,
TotalCount = totalCount,
TotalPages = totalPages,
HasNextPage = page < totalPages,
HasPreviousPage = page > 1
};
}
}Use it:
builder.Services.AddSuccessHound(options =>
{
options.UseFormatter<DefaultSuccessFormatter>();
options.UsePagination(new MyPaginationFactory());
});Note: For custom metadata structures beyond PaginationMeta, you can create your own metadata types and use ApiResponse<TData, TMeta> directly.
Use SuccessHound formatters outside of ASP.NET Core:
using SuccessHound.Abstractions;
using SuccessHound.Defaults;
// Create formatter
var formatter = new DefaultSuccessFormatter();
// Format data
var data = new { Message = "Hello, World!" };
var wrapped = formatter.Format(data);
// With metadata
var meta = new { Version = "1.0" };
var wrappedWithMeta = formatter.Format(data, meta);SuccessHound handles null data gracefully:
app.MapGet("/user/{id}", (int id, HttpContext context) =>
{
var user = FindUser(id); // May return null
return user.Ok(context);
});Response:
{
"success": true,
"data": null,
"meta": null,
"timestamp": "2025-12-15T10:30:00.000Z"
}app.MapGet("/users", (HttpContext context) =>
{
var users = new List<User>
{
new User { Id = 1, Name = "Alice" },
new User { Id = 2, Name = "Bob" }
};
return users.Ok(context);
});Response:
{
"success": true,
"data": [
{
"id": 1,
"name": "Alice"
},
{
"id": 2,
"name": "Bob"
}
],
"meta": null,
"timestamp": "2025-12-15T10:30:00.000Z"
}app.MapGet("/report", (HttpContext context) =>
{
var report = GenerateReport();
var meta = new
{
GeneratedAt = DateTime.UtcNow,
GeneratedBy = "System",
Format = "JSON",
Version = "1.0",
Filters = new { StartDate = "2025-01-01", EndDate = "2025-12-31" }
};
return report.WithMeta(meta, context);
});ToPagedResultAsync() in Pagination package)MIT License - See LICENSE for details.
Built with care for clean, consistent API responses.