Foundatio
Pluggable foundation blocks for building loosely coupled distributed apps.
Includes implementations in Redis, Azure, AWS and in memory (for development).
Why should I use Foundatio?
When we first started building Exceptionless we found a lack of great solutions (that's not to say there isn't great solutions out there) for many key pieces to building scalable distributed applications while keeping the development experience simple. Here are a few examples of why we built and use Foundatio:
- Wanted to build against abstract interfaces so that we could easily change implementations.
- Wanted the blocks to be dependency injection friendly.
- Caching: We were initially using an open source Redis cache client but then it turned into a commercial product with high licensing costs. Not only that, but there wasn't any in memory implementations so every developer was required to set up and configure Redis.
- Message Bus: We initially looked at NServiceBus (great product) but it had high licensing costs (they have to eat too) but was not OSS friendly. We also looked into MassTransit but found Azure support lacking and local set up a pain. We wanted a simple message bus that just worked locally or in the cloud.
- Storage: We couldn't find any existing project that was decoupled and supported in memory, file storage or Azure Blob Storage.
To summarize, if you want pain free development and testing while allowing your app to scale, use Foundatio!
Getting Started (Development)
Foundatio can be installed via the NuGet package manager. If you need help, please open an issue or join our Slack chat room. We’re always here to help if you have any questions!
This section is for development purposes only! If you are trying to use the Foundatio libraries, please get them from NuGet.
- You will need to have Visual Studio 2015 installed.
- Open the
Foundatio.sln
Visual Studio solution file.
Using Foundatio
The sections below contain a small subset of what's possible with Foundatio. We recommend taking a peek at the source code for more information. Please let us know if you have any questions or need assistance!
Caching
Caching allows you to store and access data lightning fast, saving you exspensive operations to create or get data. We provide four different cache implementations that derive from the ICacheClient
interface:
- InMemoryCacheClient: An in memory cache client implementation. This cache implementation is only valid for the lifetime of the process. It's worth noting that the in memory cache client has the ability to cache the last X items via the
MaxItems
property. We use this in Exceptionless to only keep the last 250 resolved geoip results. - HybridCacheClient: This cache implementation uses the
InMemoryCacheClient
and uses theIMessageBus
to keep the cache in sync across processes. - RedisCacheClient: A Redis cache client implementation.
- RedisHybridCacheClient: This cache implementation uses both the
RedisCacheClient
andInMemoryCacheClient
implementations and uses theRedisMessageBus
to keep the in memory cache in sync across processes. This can lead to huge wins in performance as you are saving a serialization operation and call to Redis if the item exists in the local cache. - ScopedCacheClient: This cache implementation takes an instance of
ICacheClient
and a stringscope
. The scope is prefixed onto every cache key. This makes it really easy to scope all cache keys and remove them with ease.
Sample
using Foundatio.Caching;
ICacheClient cache = new InMemoryCacheClient();
await cache.SetAsync("test", 1);
var value = await cache.GetAsync<int>("test");
Queues
Queues offer First In, First Out (FIFO) message delivery. We provide four different queue implementations that derive from the IQueue
interface:
- InMemoryQueue: An in memory queue implementation. This queue implementation is only valid for the lifetime of the process.
- RedisQueue: An Redis queue implementation.
- AzureServiceBusQueue: An Azure Service Bus Queue implementation.
- AzureStorageQueue: An Azure Storage Queue implementation.
Sample
using Foundatio.Queues;
IQueue<SimpleWorkItem> queue = new InMemoryQueue<SimpleWorkItem>();
await queue.EnqueueAsync(new SimpleWorkItem {
Data = "Hello"
});
var workItem = await queue.DequeueAsync();
Locks
Locks ensure a resource is only accessed by one consumer at any given time. We provide two different locking implementations that derive from the ILockProvider
interface:
- CacheLockProvider: A lock implementation that uses cache to communicate between processes.
- ThrottlingLockProvider: A lock implementation that only allows a certain amount of locks through. You could use this to throttle api calls to some external service and it will throttle them across all processes asking for that lock.
It's worth noting that all lock providers take a ICacheClient
. This allows you to ensure your code locks properly across machines.
Sample
using Foundatio.Lock;
ILockProvider locker = new CacheLockProvider(new InMemoryCacheClient(), new InMemoryMessageBus());
using (await locker.AcquireAsync("test")) {
// ...
}
ILockProvider locker = new ThrottledLockProvider(new InMemoryCacheClient(), 1, TimeSpan.FromMinutes(1));
using (await locker.AcquireAsync("test")) {
// ...
}
Messaging
Allows you to publish and subscribe to messages flowing through your application. We provide three different message bus implementations that derive from the IMessageBus
interface:
- InMemoryMessageBus: An in memory message bus implementation. This message bus implementation is only valid for the lifetime of the process.
- RedisMessageBus: A Redis message bus implementation.
- RabbitMQMessageBus: A RabbitMQ implementation.
- AzureServiceBusMessageBus: An Azure Service Bus implementation.
Sample
using Foundatio.Messaging;
IMessageBus messageBus = new InMemoryMessageBus();
await messageBus.Subscribe<SimpleMessageA>(msg => {
// Got message
});
await messageBus.PublishAsync(new SimpleMessageA { Data = "Hello" });
Jobs
Allows you to run a long running process (in process or out of process) with out worrying about it being terminated prematurely. We provide a few different ways of defining a job based on your use case.
- Jobs: All jobs must derive from the
IJob
interface. We also have aJobBase
base class you can derive from which provides a JobContext and logging. You can then run jobs by callingRunAsync()
on the job or by creating a instance of theJobRunner
class and calling one of the Run methods. The JobRunner can be used to easily run your jobs as Azure Web Jobs.
Sample
using Foundatio.Jobs;
public class HelloWorldJob : JobBase {
public int RunCount { get; set; }
protected override Task<JobResult> RunInternalAsync(JobRunContext context) {
RunCount++;
return Task.FromResult(JobResult.Success);
}
}
var job = new HelloWorldJob();
await job.RunAsync(); // job.RunCount = 1;
await job.RunContinuousAsync(iterationLimit: 2); // job.RunCount = 3;
await job.RunContinuousAsync(cancellationToken: new CancellationTokenSource(TimeSpan.FromMilliseconds(10)).Token); // job.RunCount > 10;
- Queue Processor Jobs: A queue processor job works great for working with jobs that will be driven from queued data. Queue Processor jobs must derive from
QueueJobBase<T>
class. You can then run jobs by callingRunAsync()
on the job or passing it to theJobRunner
class. The JobRunner can be used to easily run your jobs as Azure Web Jobs.
Sample
using Foundatio.Jobs;
public class HelloWorldQueueJob : QueueJobBase<HelloWorldQueueItem> {
public int RunCount { get; set; }
public HelloWorldQueueJob(IQueue<HelloWorldQueueItem> queue) : base(queue) {}
protected override Task<JobResult> ProcessQueueEntryAsync(QueueEntryContext<HelloWorldQueueItem> context) {
RunCount++;
return Task.FromResult(JobResult.Success);
}
}
public class HelloWorldQueueItem {
public string Message { get; set; }
}
// Register the queue for HelloWorldQueueItem.
container.RegisterSingleton<IQueue<HelloWorldQueueItem>>(() => new InMemoryQueue<HelloWorldQueueItem>());
// To trigger the job we need to queue the HelloWorldWorkItem message.
// This assumes that we injected an instance of IQueue<HelloWorldWorkItem> queue
var job = new HelloWorldQueueJob();
await job.RunAsync(); // job.RunCount = 0; The RunCount wasn't incremented because we didn't enqueue any data.
await queue.EnqueueAsync(new HelloWorldWorkItem { Message = "Hello World" });
await job.RunAsync(); // job.RunCount = 1;
await queue.EnqueueAsync(new HelloWorldWorkItem { Message = "Hello World" });
await queue.EnqueueAsync(new HelloWorldWorkItem { Message = "Hello World" });
await job.RunUntilEmptyAsync(); // job.RunCount = 3;
- Work Item Jobs: A work item job will run in a job pool among other work item jobs. This type of job works great for things that don't happen often but should be in a job (Example: Deleting an entity that has many children.). It will be triggered when you publish a message on the
message bus
. The job must derive from theWorkItemHandlerBase
class. You can then run all shared jobs viaJobRunner
class. The JobRunner can be used to easily run your jobs as Azure Web Jobs.
Sample
using System.Threading.Tasks;
using Foundatio.Jobs;
public class HelloWorldWorkItemHandler : WorkItemHandlerBase {
public override async Task HandleItemAsync(WorkItemContext ctx) {
var workItem = ctx.GetData<HelloWorldWorkItem>();
// We can report the progress over the message bus easily.
// To recieve these messages just inject IMessageSubscriber
// and Subscribe to messages of type WorkItemStatus
await ctx.ReportProgressAsync(0, "Starting Hello World Job");
await Task.Delay(TimeSpan.FromSeconds(2.5));
await ctx.ReportProgressAsync(50, String.Format("Reading value"));
await Task.Delay(TimeSpan.FromSeconds(.5));
await ctx.ReportProgressAsync(70, String.Format("Reading value."));
await Task.Delay(TimeSpan.FromSeconds(.5));
await ctx.ReportProgressAsync(90, String.Format("Reading value.."));
await Task.Delay(TimeSpan.FromSeconds(.5));
await ctx.ReportProgressAsync(100, workItem.Message);
}
}
public class HelloWorldWorkItem {
public string Message { get; set; }
}
// Register the shared job.
var handlers = new WorkItemHandlers();
handlers.Register<HelloWorldWorkItem, HelloWorldWorkItemHandler>();
// Register the handlers with dependency injection.
container.RegisterSingleton(handlers);
// Register the queue for WorkItemData.
container.RegisterSingleton<IQueue<WorkItemData>>(() => new InMemoryQueue<WorkItemData>());
// The job runner will automatically look for and run all registered WorkItemHandlers.
new JobRunner(container.GetInstance<WorkItemJob>(), instanceCount: 2).RunInBackground();
// To trigger the job we need to queue the HelloWorldWorkItem message.
// This assumes that we injected an instance of IQueue<WorkItemData> queue
// NOTE: You may have noticed that HelloWorldWorkItem doesn't derive from WorkItemData.
// Foundatio has an extension method that takes the model you post and serializes it to the
// WorkItemData.Data property.
await queue.EnqueueAsync(new HelloWorldWorkItem { Message = "Hello World" });
File Storage
We provide four different file storage implementations that derive from the IFileStorage
interface:
- InMemoryFileStorage: An in memory file implementation. This file storage implementation is only valid for the lifetime of the process.
- FolderFileStorage: An file storage implementation that uses the hard drive for storage.
- AzureFileStorage: An Azure Blob Storage implementation.
- S3Storage: An AWS S3 Storage implementation.
We recommend using all of the IFileStorage
implementations as singletons.
Sample
using Foundatio.Storage;
IFileStorage storage = new InMemoryFileStorage();
await storage.SaveFileAsync("test.txt", "test");
string content = await storage.GetFileContentsAsync("test.txt")
Metrics
We provide multiple implementations that derive from the IMetricsClient
interface:
- InMemoryMetricsClient: An in memory metrics implementation.
- RedisMetricsClient: An Redis metrics implementation.
- StatsDMetricsClient: An statsd metrics implementation.
- MetricsNETClient: An Metrics.NET implementation.
We recommend using all of the IMetricsClient
implementations as singletons.
Sample
await metrics.CounterAsync("c1");
await metrics.GaugeAsync("g1", 2.534);
await metrics.TimerAsync("t1", 50788);
Logging
We provide a fluent logging api that can be used to log messages throughout your application. This is really great because it allows you to log to different sources like NLog and change it at a later date without updating your whole application to use the latest and greatest logging framework on the market.
By default the logger will not write to anything, but you can configure what to write to adding registering a logging provider. We provide a few logging providers out of the box (in memory, xUnit and NLog).
Sample
ILoggerFactory loggerFactory = new LoggerFactory();
ILogger log = loggerFactory.CreateLogger("Program");
log.Info("Application starting up"); // OR
log.Info().Message("Application starting up").Write();
log.Error(ex, "Writing a captured exception out to the log."); // Or
log.Error().Exception(ex).Message("Writing a captured exception out to the log.").Write();
Sample Application
We have both slides and a sample application that shows off how to use Foundatio.
Sponsors
Roadmap
This is a list of high level things that we are planning to do: