A presentation for the Fox Valley .NET Web Development Meetup.
Also, check out the accompanying slides.
Create the 'app' directory:
mkdir app
cd app
export appdir=`pwd`
dotnet new web -n FoxValleyMeetup -o FoxValleyMeetup
cd FoxValleyMeetup
Add dotnet-watch, by opening the .csproj
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.DotNet.Watcher.Tools" Version="2.0.0" />
</ItemGroup>
Fire it up, and check for "Hello World!" at http://localhost:5000
cd FoxValleyMeetup
export ASPNETCORE_ENVIRONMENT=Development # use `set` if on windows
dotnet watch run
Create a new class library
cd $appdir
dotnet new classlib -n FoxValleyMeetup.Web
cd FoxValleyMeetup.Web
rm Class1.cs
Add ServiceStack packages (only the ServiceStack package is needed, but we're going to look at lots of features so we'll bring in all the core packages for now to save us some time)
dotnet add package ServiceStack
dotnet add package ServiceStack.Server
dotnet add package ServiceStack.OrmLite
dotnet add package ServiceStack.OrmLite.SqLite
dotnet add package ServiceStack.OrmLite.SqlServer
dotnet add package ServiceStack.OrmLite.MySql
dotnet add package ServiceStack.OrmLite.Postgresql
dotnet add package ServiceStack.Api.OpenApi
dotnet restore
Reference the classlib in the FoxValleyMeetup project file
dotnet add $appdir/FoxValleyMeetup/FoxValleyMeetup.csproj reference FoxValleyMeetup.Web.csproj
Create a ServiceStack AppHost class. The AppHost is:
- A singleton
- The central nervous system with a quantity of overrides to extend the framework in every possible way
public class AppHost : AppHostBase
{
public AppHost(): base("FoxValleyMeetup", typeof(AppHost).Assembly)
{
}
public override void Configure(Container container)
{
}
}
Hook it up in the FoxValleyMeetup Startup.cs class
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseServiceStack(new AppHost());
}
Move the "Hello World!" message to the ServiceStack AppHost
public override async Task ProcessRequest(HttpContext context, Func<Task> next)
{
await context.Response.WriteAsync("Hello World!");
}
Refresh the browser at http://localhost:5000
Architecture: http://docs.servicestack.net/architecture-overview
http://docs.servicestack.net/order-of-operations
- Incoming request
- Route/params/content-type/verb recognition
- RawHttpHandlers (IHttpHandler)
- CatchAllHandler
- PreRequestFilters (before request body is deserialized into request DTO)
- Request binding to request DTOs
- Request converters (substitute the request DTO for another)
- Request filters (good place to do validation/authentication or populate request items bag)
- Service execution (w/ OnBeforeExecute, OnAfterExecute, HandleException)
- Outgoing response:
- Response converters (substitute the response DTO for another)
- Response filters (modify outgoing headers)
- OnEndRequest, OnEndRequestCallbacks
Comment out the ProcessRequest override in the AppHost file, which would short-circuit the ServiceStack pipeline.
Let's create a basic service. Create a PingService
class
public class PingService: Service
{
public IHostingEnvironment Env { get; set; } // aspnet core DI works out-of-the-box
public object Any(PingRequest request)
{
return new PingResponse {
ServerTime = DateTime.UtcNow,
ApplicationName = Env.ApplicationName,
EnvironmentName = Env.EnvironmentName };
}
}
[Route("/ping", "GET,PUT,POST")]
public class PingRequest: IReturn<PingResponse>
{
}
public class PingResponse {
public DateTime ServerTime { get; set; }
public string ApplicationName { get; set; }
public string EnvironmentName { get; set; }
}
Notice the [Route] attribute, and the Any method. Navigate to http://localhost:5000/ping.
There are several ways of mapping request paths to their respective DTOs. The RouteAttribute
attribute is probably the most
common and easiest to wire up. But sometimes you may want to configure the path with a more fluent interface. Use the Route table instead.
- In the apphost Configure method, use the Route table
this.Routes.Add(typeof(PingRequest), "/ping", "GET,PUT,POST");
Go to http://localhost:5000/metadata
Integrating typed clients, without sharing the actual assemblies, is easy:
Out of the box, you get a bunch of format support:
- xml
- json
- csv
Let's add another:
- yaml
Make sure YamlDotNet is added to the FoxValleyMeetup.Web project
dotnet add package YamlDotNet
Create a YamlFormat
class
public class YamlFormat : IPlugin
{
public YamlFormat(bool usePlainText = false, bool useRequestTypeAsName = false)
{
UsePlainText = usePlainText;
UseRequestTypeAsName = useRequestTypeAsName;
}
public bool UsePlainText { get; }
public bool UseRequestTypeAsName { get; }
public void Register(IAppHost appHost)
{
//Register the 'application/yaml' content-type and serializers (format is inferred from the last part of the content-type)
appHost.ContentTypes.Register(MimeTypes.Yaml, YamlSerializer.SerializeToStream, YamlSerializer.DeserializeFromStream);
//Add a response filter to add a 'Content-Disposition' header so browsers treat it natively as a .yaml file
appHost.GlobalResponseFilters.Add((req, res, dto) =>
{
if (req.ResponseContentType == MimeTypes.Yaml)
{
if (UsePlainText)
{
res.RemoveHeader("Content-Type");
res.AddHeader("Content-Type", MimeTypes.PlainText);
}
else
{
if (UseRequestTypeAsName)
{
res.AddHeader(HttpHeaders.ContentDisposition, $"attachment;filename={req.OperationName}.yaml");
}
}
}
});
}
}
public class YamlSerializer
{
public static object DeserializeFromStream(Type type, Stream fromStream)
{
using (var reader = new StreamReader(fromStream))
{
return YamlSerializer.SerializeFromReader(type, reader);
}
}
private static object SerializeFromReader(Type type, StreamReader reader)
{
var serializer = new YamlDotNet.Serialization.Deserializer();
return serializer.Deserialize(reader, type);
}
public static void SerializeToStream(IRequest requestContext, object request, Stream stream)
{
YamlSerializer.SerializeToStream(request, stream);
}
public static void SerializeToStream(object request, Stream stream)
{
using (var writer = new StreamWriter(stream))
{
YamlSerializer.SerializeToWriter(request, writer);
}
}
private static void SerializeToWriter(object request, StreamWriter writer)
{
var serializer = new YamlDotNet.Serialization.Serializer();
serializer.Serialize(writer, request);
}
}
Notice that the class extends the IPlugin interface. This is the most common way of extending ServiceStack.
Also notice how we used a GlobalResponseFilter to manipulate the headers in the request.
Now add the YamlFormat to the application, by adding it in the AppHost class.
// add yaml format
Plugins.Add(new YamlFormat(true));
Go to http://localhost:5000/metadata
- Notice the new YAML format
ServiceStack lets us configure several dozen core settings of the framework.
Inside the AppHost.Configure
method, add the following as an example:
SetConfig(new HostConfig
{
DebugMode = true, // enables some debugging features
EnableFeatures = Feature.All.Remove(Feature.Csv), // removes the CSV format
HandlerFactoryPath = "/api" // moves the ServiceStack api surface under a sub-route
});
Now, navigate to http://localhost:5000/api/metadata and /api/ping instead of the previous routes.
Let's comment out the factory path before continuing to the next step.
Let's add a UI to start documenting our services, and also give us an easy way of interacting with the services.
dotnet add package ServiceStack.Api.OpenApi
In the AppHost
class, add the OpenApiFeature plugin.
Plugins.Add(new OpenApiFeature() {
UseCamelCaseSchemaPropertyNames = true
});
Now, browse to http://localhost:5000/swagger-ui/
http://docs.servicestack.net/authentication-and-authorization#auth-providers
- Wire up authentication inside the AppHost as a Plugin
Plugins.Add(new AuthFeature(() => new AuthUserSession(), new IAuthProvider[] {
new CredentialsAuthProvider(),
new BasicAuthProvider(),
new ApiKeyAuthProvider()
}) {
IncludeAssignRoleServices = true,
IncludeRegistrationService = true,
SaveUserNamesInLowerCase = true
});
var authRepository = new InMemoryAuthRepository();
Register<IAuthRepository>(authRepository);
- Add authenticated user info to PingService
public IAuthSession UserSession { get; set; } // use base.GetSession(true) to populate it
-
Try registering a user, authenticating, and see validation in action in the OpenApi interface
-
Organize the documentation by adding OpenApi tags using runtime attributes
(new [] { typeof(Authenticate), typeof(Register), typeof(GetApiKeys), typeof(RegenerateApiKeys) }).Each(t => t.AddAttributes(new TagAttribute("auth")));
(new [] { typeof(AssignRoles), typeof(UnAssignRoles) }).Each(t => t.AddAttributes(new TagAttribute("roles")));
- create a users api
Add OrmLite to the project and SqLite (could also use SqlServer, MySql, Oracle, Postgres). OrmLite is a database agnostic ORM
dotnet add package ServiceStack.OrmLite
dotnet add package ServiceStack.OrmLite.SqLite
dotnet add package ServiceStack.Server
dotnet restore
Now create a database registration in AppHost
container.Register<IDbConnectionFactory>(c => { return new OrmLiteConnectionFactory("db.sqlite", SqliteDialect.Provider); });
var dbFactory = TryResolve<IDbConnectionFactory>();
Also, switch the auth repository to use the database
var authRepository = new OrmLiteAuthRepository(dbFactory);
authRepository.InitSchema();
authRepository.InitApiKeySchema();
RegisterAs<OrmLiteCacheClient, ICacheClient>();
TryResolve<ICacheClient>().InitSchema();
Now create an api that only lets authenticated users query the users in the system
[Authenticate]
public class UsersService : Service
{
public async Task<object> Get(UsersRequest request)
{
return (await Db.SelectAsync<UserAuth>(Db.From<UserAuth>().OrderBy(u => u.UserName).ThenBy(u => u.DisplayName)))
.Map(u => u.To<UserInfo>() new UserInfo().PopulateWithNonDefaultValues(u));
}
}
public class UserInfo
{
public virtual int Id { get; set; }
public virtual string UserName { get; set; }
public virtual string Email { get; set; }
public virtual string PhoneNumber { get; set; }
public virtual string DisplayName { get; set; }
}
[Route("/users", "GET")]
public class UsersRequest: IReturn<List<UserInfo>>
{
}
Plugins.Add(new AutoQueryFeature { MaxLimit = 100 });