I have library API in ASP NET Core 3.1 MVC where users can borrow, return and follow borrowed book status. I want to create email notification so when book is returned, all the users that are following this specific book status will recieve email notification that its available.
I want to use RabbitMQ with MassTransit and handle the emails on different web service.
This is my code that is sending messages to the rabbit queue:
public async Task SendNotificationStatus(Book book, CancellationToken cancellationToken)
{
var endpoint = await _bus.GetSendEndpoint(new System.Uri($"rabbitmq://{_rabbitHostName}/library-notifications"));
var bookSpectators = await _userRepository.GetSpectatorsByBookId(book.Id, cancellationToken);
foreach (var user in bookSpectators)
{
NotifyStatusReturn rabbitMessage = new NotifyStatusReturn
{
NotificationType = NotificationTypes.BookReturn,
RecipientAddress = user.EmailAddress,
RecipientLogin = user.Login,
SentDate = DateTime.UtcNow,
BookTitle = book.Title
};
await endpoint.Send(rabbitMessage);
}
}
And when it comes to the notifications service i've created the project with templates provided on MassTransit website - https://masstransit-project.com/usage/templates.html#installation
First I ran dotnet new mtworker -n LibraryNotifications and then after going inside the project folder dotnet new mtconsumer
I've added MassTransit.RabbitMq 8.0.0 package via NugerPackage Manager.
In Contracts folder created via mtconsumer template i ve changed the name of the record to NotifyStatusReturn which look like this:
namespace Contracts
{
public record NotifyStatusReturn
{
public string NotificationType { get; set; }
public string RecipientAddress { get; set; }
public string RecipientLogin { get; set; }
public DateTime SentDate { get; set; }
public string BookTitle { get; set; }
}
}
And in Program.cs swapped the x.UsingInMemory() to
x.UsingRabbitMq((context, cfg) =>
{
cfg.Host("localhost", "/", h =>
{
h.Username("guest");
h.Password("guest");
});
cfg.ConfigureEndpoints(context);
});
When i return the book, the message goes into library-notifications_skipped queue as a dead-letter. All the bindings seems okay for me and i really dont know what is the reason that my messages are not being consumed. Could anybody help me with this issue?
As per the documentation:
MassTransit uses the full type name, including the namespace, for message contracts. When creating the same message type in two separate projects, the namespaces must match or the message will not be consumed.
Make sure that your message type has the same namespace/type in each project.
Related
I am trying to learn the basics of ASP.NET. I decided to try to make a telegram bot and my first step is to get it to loop back my messages to it. I am using Telegram.Bot as my client. I set up the project using VS Code connected remotely to a Ubuntu 20.04 machine next to my windows PC (which I am working from). dotnet --version says I am running 3.1.4 (on the linux machine). I ran dotnet new mvc -o telegram-odin. Then I added the BotController class seen below. I have used this class to test various endpoints and methods. Just feeling out how to do things. I have my telegram bot token stored in an environment variable on the linux machine. I am using ngrok to allow telegram's webhooks to post back to me.
My issue is that I can't seem to get any post endpoints to correctly accept objects. I tried using Telegram.Bot's webhook example, but this just doesn't work. Update is always null, and the Request.Body is always empty. I have searched around and found other people with issues of null objects when using the [FromBody] attribute, but they all talked about receiving primitive types instead of objects. I did see that someone mentioned that something called "Middleware" might be consuming the body when I use [FromBody]. So I removed it and tried to parse the Request.Body directly. I found that I would get a default (the same as the result of new Update()) value for update, but more importantly the Request.Body has the JSON data that I can grab and parse. Theoretically I am in business now and can just deserialize it. However, I feel like I should be able to let ASP.NET just handle that for me. So something must be wrong.
My configured services.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers().AddNewtonsoftJson();
services.AddControllersWithViews();
services.AddSingleton<TelegramBotClient>(new TelegramBotClient(Program.GetBotToken()));
}
My Controller class.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Telegram.Bot;
using Telegram.Bot.Types;
using Newtonsoft.Json;
using System.IO;
namespace telegram_odin.Controllers
{
public class BotController : ControllerBase
{
private readonly ILogger<BotController> _logger;
private readonly TelegramBotClient _telegramClient;
public BotController(ILogger<BotController> logger, TelegramBotClient telegramClient)
{
_logger = logger;
_telegramClient = telegramClient;
// I know I am resetting the endpoint every time one of these endpoints is called.
// I will fix this later.
var botClient = _telegramClient;
botClient.SetWebhookAsync(GetWebhookInEndpoint());
}
public async Task<string> Index()
{
var botClient = _telegramClient;
var me = await botClient.GetMeAsync();
return me.ToString();
}
public async Task<string> GetWebhookInfo()
{
var botClient = _telegramClient;
var webhookInfo = await botClient.GetWebhookInfoAsync();
return JsonConvert.SerializeObject(webhookInfo).ToString();
}
[HttpPost]
public async Task<IActionResult> Update(Update update)
{
try
{
if (update is null)
{
_logger.LogInformation("update is null");
}
else
{
string updateAsJson = JsonConvert.SerializeObject(update);
_logger.LogInformation($"update as json - {updateAsJson}");
}
string body = "";
using (StreamReader reader =new StreamReader(Request.Body))
{
body = await reader.ReadToEndAsync();
}
_logger.LogInformation($"update body - {body}");
return Ok();
}
catch (Exception ex)
{
return Problem(ex.Message);
}
}
// I know this is the wrong place for this method.
// Quick and dirty is the way to learn.
private string GetWebhookInEndpoint()
{
// I wrote a helper to pull the https Uri by asking my local ngrok server about its' open tunnels.
var ngrokUri = NgrokHelpers.GetNgrokUri();
var wholeUri = new Uri(ngrokUri, "Bot/Update");
_logger.LogInformation($"generated webhook endpoint - {wholeUri.ToString()}");
return wholeUri.ToString();
}
}
}
This is Telegram.Bot's Update definition.
#region Assembly Telegram.Bot, Version=15.7.1.0, Culture=neutral, PublicKeyToken=null
// Telegram.Bot.dll
#endregion
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Telegram.Bot.Types.Enums;
using Telegram.Bot.Types.Payments;
namespace Telegram.Bot.Types
{
//
// Summary:
// This object represents an incoming update.
//
// Remarks:
// Only one of the optional parameters can be present in any given update.
[JsonObject(MemberSerialization.OptIn, NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
public class Update
{
public Update();
//
// Summary:
// The update's unique identifier. Update identifiers start from a certain positive
// number and increase sequentially. This ID becomes especially handy if you're
// using Webhooks, since it allows you to ignore repeated updates or to restore
// the correct update sequence, should they get out of order.
[JsonProperty("update_id", Required = Required.Always)]
public int Id { get; set; }
//
// Summary:
// Optional. New incoming message of any kind — text, photo, sticker, etc.
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public Message Message { get; set; }
//
// Summary:
// Optional. New version of a message that is known to the bot and was edited
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public Message EditedMessage { get; set; }
//
// Summary:
// Optional. New incoming inline query
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public InlineQuery InlineQuery { get; set; }
//
// Summary:
// Optional. The result of a inline query that was chosen by a user and sent to
// their chat partner
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public ChosenInlineResult ChosenInlineResult { get; set; }
//
// Summary:
// Optional. New incoming callback query
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public CallbackQuery CallbackQuery { get; set; }
//
// Summary:
// Optional. New incoming channel post of any kind — text, photo, sticker, etc.
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public Message ChannelPost { get; set; }
//
// Summary:
// Optional. New version of a channel post that is known to the bot and was edited
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public Message EditedChannelPost { get; set; }
//
// Summary:
// Optional. New incoming shipping query. Only for invoices with flexible price
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public ShippingQuery ShippingQuery { get; set; }
//
// Summary:
// Optional. New incoming pre-checkout query. Contains full information about checkout
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public PreCheckoutQuery PreCheckoutQuery { get; set; }
//
// Summary:
// New poll state. Bots receive only updates about polls, which are sent or stopped
// by the bot
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public Poll Poll { get; set; }
//
// Summary:
// Optional. A user changed their answer in a non-anonymous poll. Bots receive new
// votes only in polls that were sent by the bot itself.
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public PollAnswer PollAnswer { get; set; }
//
// Summary:
// Gets the update type.
//
// Value:
// The update type.
public UpdateType Type { get; }
}
}
So I am trying to write a server with a database which would host a site similar to reddit: with users, groups, posts and comments.
As this is my first time developing backend, I tried googling and found ASP.NET Core 3.1 Web API with Entity Framework to be "good". (I use SQL Server as the database server if that's relevant)
Now I went with the code first method, and created the tables with a migration.
(ERD)
I used the built in tool to create the controllers.
If I used this JSON to create a post entity (with HTTP POST), the AuthorId and the LocationId stay on NULL (weather I use "" for the numbers or not). I created the Group and the User it would refer to earlier.
The JSON:
{
"title": "Post",
"posttext": "text",
"creatorid": "1",
"locationid": "1",
"timeofpost": "2020-01-12"
}
The Post class:
using System;
using System.Collections.Generic;
namespace CedditBackend.Models
{
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
#nullable enable
public string? PostText { get; set; }
public byte[]? Content { get; set; }
public ICollection<Comment>? Comments { get; set; }
#nullable disable
public User Creator { get; set; }
public Group Location { get; set; }
public DateTime TimeOfPost { get; set; }
}
}
The autogenerated HTTPPOST in the controller:
[HttpPost]
public async Task<ActionResult<Post>> PostPost(Post post)
{
_context.Posts.Add(post);
await _context.SaveChangesAsync();
return CreatedAtAction("GetPost", new { id = post.Id }, post);
}
On the other hand if I try and create my own post class from the JSON (basically deserializing it), I get an error that "a cycle was detected". This I tried googling, but only found that with an earlier version (I guess) I could just turn that off.
Still if I open the database, it shows me that the values of LocationId and AuthorId are not NULL, but if I try to get the posts of the Users (with an HTTP GET), all I get is the same error message.
My rewritten HTTPPOST
[HttpPost]
public async Task<ActionResult<Post>> PostPost(Object obj)
{
Dictionary<string, object> dict = JsonConvert.DeserializeObject<Dictionary<string, object>>(obj.ToString());
Post post = new Post();
post.Title = dict["title"].ToString();
post.PostText = dict["posttext"].ToString();
post.TimeOfPost = DateTime.Parse(dict["timeofpost"].ToString());
post.Location = _context.Groups.FirstOrDefault(x => x.Id == int.Parse(dict["locationid"].ToString()));
post.Creator = _context.Users.FirstOrDefault(x => x.Id == int.Parse(dict["creatorid"].ToString()));
post.Creator.Posts.Add(post);
post.Location.Posts.Add(post);
_context.Posts.Add(post);
await _context.SaveChangesAsync();
return CreatedAtAction("GetPost", new { id = post.Id }, post);
}
I tried to google this whole stuff in several ways, but found nothing. Am I missing something obvious?
Edit: the User class (which I think causes the cycle reference)
using System.Collections.Generic;
namespace CedditBackend.Models
{
public class User
{
public int Id { get; set; }
public string UserName { get; set; }
#nullable enable
public ICollection<Post>? Posts { get; set; } = new List<Post>();
public ICollection<Comment>? Comments { get; set; } = new List<Comment>();
public ICollection<UserGroup>? UserGroups { get; set; }
#nullable disable
}
}
It looks like you're running into the same problem I currently have.
The new JSON library System.Text.JSON is currently unable to handle One-to-Many relationships it seems. They are currently tracking the issue here, but a fix isn't planned until .NET 5. I haven't found a solution at this point but I will update this answer if I do.
UPDATE
Found a workaround, at least for now.
Install Newtonsoft MVC Extensions
PM> Install-Package Microsoft.AspNetCore.Mvc.NewtonsoftJson -Version 3.1.1
Edit ConfigureServices method in Startup.cs
services
.AddControllers()
.AddNewtonsoftJson()
Now configure the loop handling in Newtonsoft
services
.AddControllers()
.AddNewtonsoftJson(options =>
{
options.SerializerSettings.ReferenceLoopHandling =
Newtonsoft.Json.ReferenceLoopHandling.Ignore;
});
That should be enough to get it working. Keep in mind if you have any custom JsonConverters you will have to change to the converters provided in the Newtonsoft library to get them to work.
Try removing these lines.
post.Creator.Posts.Add(post);
post.Location.Posts.Add(post);
You've already defined the Creator and Location of your post. Adding it to their collections would make it a circular reference.
Recently we replaced our internal messaging library with Apache Thrift. In our setup we have a couple of c# services and several c++ 'clients' who connect to them. In the beginning we used TThreadPoolServer for the server implementation.
_server = new TThreadPoolServer(processor, serverTransport);
Because it worked so well we also replaced our last server. This server needs to communicate with more than 400 clients. The problem we saw that as soon client number 100 connected our the server stopped accepting new connections and it did not even call c# timers. It kind of seem to choke till first clients disconnected.
By reading this SO Large number of simulteneous connections in thrift we decided to change to TThreadedServer and increased max number of threads to 500 and we have a working solution.
_server = new TThreadedServer(processor
, serverTransport
, new TTransportFactory()
, new TTransportFactory()
, new TBinaryProtocol.Factory()
, new TBinaryProtocol.Factory()
, 500
, DebugLogThriftServer);
Still I would like to have the TNonblockingServer as it is implemented in the Thrift c++ library has. I was wondering if someone already wrote one for c#.
This is an RPC framework that uses the standard thrift Protocol, and it is the same effect as using thrift IDL to define the service, that is, thrify can be compatible with code that uses thrift IDL, which is very helpful for cross-platform.
[ThriftStruct]
public class LogEntry
{
[ThriftConstructor]
public LogEntry([ThriftField(1)]String category, [ThriftField(2)]String message)
{
this.Category = category;
this.Message = message;
}
[ThriftField(1)]
public String Category { get; }
[ThriftField(2)]
public String Message { get; }
}
[ThriftService("scribe")]
public interface IScribe
{
[ThriftMethod("getMessages")]
List<LogEntry> GetMessages();
[ThriftMethod]
ResultCode Log(List<LogEntry> messages);
}
public class Scribe : IScribe
{
public List<LogEntry> GetMessages()
{
return new List<LogEntry>
{
new LogEntry { Category = "c1", Message = Guid.NewGuid().ToString() },
new LogEntry { Category = "c2", Message = Guid.NewGuid().ToString() },
new LogEntry { Category = "c3", Message = Guid.NewGuid().ToString() }
};
}
public ResultCode Log(List<LogEntry> messages)
{
return ResultCode.TRY_LATER;
}
}
you can try it: https://github.com/endink/Thrifty
Here: Recommended ServiceStack API Structure and here: https://github.com/ServiceStack/ServiceStack/wiki/Physical-project-structure are recommendations for how to structure your projects for C# clients to reuse DTOs.
Apparently this is done by including a dll of the DTO assembly. I have searched the web for one example, just Hello World that uses a separate assembly DTO for a C# client in ServiceStack. Perhaps I should be able to break this out myself but so far it has not proven that easy.
Almost all client descriptions are for generic and non-typed JSON or other non-DTO based clients. No one appears interested in typed C# clients like I am (even the ServiceStack documentation I have found). So I thought this would be a good question even if I figure it out myself in the end.
To be clear, I have built and run the Hello World example server. I have also used a browser to attach to the server and interact with it. I have also created a client empty project that can call
JsonServiceClient client = new JsonServiceClient(myURL);
Then I tried to copy over my DTO definition without the assembly DLL as I don't have one. I get ResponseStatus is undefined.
Clearly there is something missing (it appears to be defined in ServiceStack.Interfaces.dll) and if I could create a dll of the DTO I think it would resolve all references.
Can anyone give insight into how to create the DTO assembly for the simple Hello World?
Edited to add code:
using ServiceStack.ServiceClient.Web;
namespace TestServiceStack
{
class HelloClient
{ public class HelloResponse
{
public string Result { get; set; }
public ResponseStatus ResponseStatus { get; set; } //Where Exceptions get auto-serialized
}
//Request DTO
public class Hello
{
public string Name { get; set; }
}
HelloResponse response = client.Get(new Hello { Name = "World!" });
}
}
Where the ResponceStatus is undefined.
I was able to find the missing symbol ResponseStatus by adding:
using ServiceStack.ServiceInterface.ServiceModel;
Here is the full code that built. Keep in mind that I found out something else in the process. Once this built it then failed because I was using a DTO from a .NET 4.0 environment in a .NET 3.5 environment. But that is an unrelated issue. Also note that this test code does nothing with the response, it is just an example to get the build working.
using ServiceStack.ServiceClient;
using ServiceStack.ServiceInterface;
using ServiceStack.Text;
using ServiceStack.Service;
using ServiceStack.ServiceHost;
using ServiceStack.WebHost;
using ServiceStack;
using ServiceStack.ServiceClient.Web;
using RestTestRoot; // This is the name of my DTO assembly. You will need to insert your own here.
using ServiceStack.ServiceInterface.ServiceModel;
namespace WebApplicationRoot
{
class HelloClient
{
JsonServiceClient hello_client;
//Request DTO
public class Hello
{
public string Name { get; set; }
}
//Response DTO
public class HelloResponse
{
public string Result { get; set; }
public ResponseStatus ResponseStatus { get; set; } //Where Exceptions get auto-serialized
}
//Can be called via any endpoint or format, see: http://mono.servicestack.net/ServiceStack.Hello/
public class HelloService : Service
{
public object Any(Hello request)
{
return new HelloResponse { Result = "Hello, " + request.Name };
}
}
//REST Resource DTO
[Route("/todos")]
[Route("/todos/{Ids}")]
public class Todos : IReturn<List<Todo>>
{
public long[] Ids { get; set; }
public Todos(params long[] ids)
{
this.Ids = ids;
}
}
[Route("/todos", "POST")]
[Route("/todos/{Id}", "PUT")]
public class Todo : IReturn<Todo>
{
public long Id { get; set; }
public string Content { get; set; }
public int Order { get; set; }
public bool Done { get; set; }
}
public HelloClient(){
// ServiceStack gateway = new ServiceStack.ClientGateway(
// location.protocol + "//" + location.host + '/ServiceStack.Examples.Host.Web/ServiceStack/');
hello_client = new JsonServiceClient("http://tradetree2.dnsapi.info:8080/");
hello_client.Get<HelloResponse>("/hello/MyTestWorld!");
}
}
}
Environment is Visual Studio 2012, ServiceStack, ASP.NET Web Application Project (followed https://github.com/ServiceStack/ServiceStack/wiki/Create-your-first-webservice)
Looking through some of the classes in ServiceStack.Examples, I noticed that most of the services contain only one method. Either some override on Execute() or, if a REST service, some override of OnPost/Get/Put/Delete().
How should I approach making a full API set, if I have tens of functions I need implemented RegisterUser(), RemoveUser(), AddFriend(), RemoveFriend() ... One service per method?
public RegisterUserService : IService<User> { public object Execute(User> dto) { ... } }
public RemoveUserService : IService<User> { public object Execute(User> dto) { ... } }
public AddFriendService : IService<Friend> { public object Execute(Friend dto) { ... } }
public RemoveFriendService: IService<RequestDTO4> { public object Execute(Friend dto) { ... } }
I'm pretty lost on how to begin implementing a full API set. I've read the first and second wiki page on 'Creating your first webservice', which I've copied to make 1 service method. But now I want to make 10 or 40 service methods and I'm not sure how to do that.
I noticed that implementing from IRestService<T> allows you up to 4 methods instead of the one Execute() method, simply because each method corresponds to a different HTTP verb. So is there something like that I could write? Basically something like:
public MyService : IService/IRestService/ServiceBase?<User>
{
public object AddUser(User user) { }
public object RemoveUser(User user) { }
public object ModifyUser(User user) { }
}
Just looking for something that doesn't necessarily have to contain all methods in one service class, but as many as reasonably possible ... do I really have to create 1 service for each service method?
Note on pursuing a strictly RESTful architecture: I only read up a little on REST, but it seems like I'd have to strictly follow rules like: treat everything as a resource even if you have to re-design your models, no verbs in the URL names (/Friends, not /GetFriends because REST gives you OnGet(), OnPost(), OnPut(), and OnDelete() ... basically I'm interested in the easiest, quickest, and most painless way of implementing a a few dozen service methods. It's a personal project, so the requirements won't vary too much.
Thanks in advance for guiding me through this first step.
EDIT: Just saw this related question: How to send commands using ServiceStack?
Mythz said there's no "ServiceStack way" to design. The guy's question is pretty much like mine. I'm wondering how to stack a lot of service methods in a service.
EDIT 2: Just saw Need help on servicestack implementation, and Separate or combined ServiceStack services?.
I just tested the code below successfully with working routes:
[Route("/registerUser/setEmail/{Email}")]
[Route("/registerUser/setPassword/{Password}")]
[Route("/registerUser/setPhoneNumber/{PhoneNumber}")]
[Route("/lalal2395823")]
[Route("/test3234/test23423511")]
public class RegisterUser
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Nickname { get; set; }
public string PhoneNumber { get; set; }
public string Email { get; set; }
public string Password { get; set; }
}
But what I'd like is for each [Route("path")] to go to a different method, instead of having them all parsed in Execute() and having to parse which string isn't null or empty.
My Solution
I decided to take Rickard's advice and make a proper REST API, because it seems simpler and cleaner in the end.
This is now my class using the new ServiceStack API (new as of 9/24/12):
using UserModel = Project.Model.Entities.User;
[Route("/User", "POST")]
[Route("/User/{FirstName}", "POST")]
[Route("/User/{FirstName}/{LastName}", "POST")]
[Route("/User/{FirstName}/{LastName}/{Nickname}", "POST")]
[Route("/User/{FirstName}/{LastName}/{Nickname}/{PhoneNumber}", "POST")]
[Route("/User/{FirstName}/{LastName}/{Nickname}/{PhoneNumber}/{Email}", "POST")]
public class CreateUser : IReturn<UserModel>
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Nickname { get; set; }
public string PhoneNumber { get; set; }
public string Email { get; set; }
public string Password { get; set; }
}
public class CreateUserService : Service
{
public object Post(CreateUser request)
{
try
{
using (var session = FluentNHibernateHelper.OpenSession())
{
using (var transaction = session.BeginTransaction())
{
var user = new UserModel()
{
FirstName = request.FirstName,
LastName = request.LastName,
Nickname = request.Nickname,
PhoneNumber = request.PhoneNumber,
Email = request.Email,
Password = request.Password,
};
session.SaveOrUpdate(user);
transaction.Commit();
return user;
}
}
}
catch
{
throw;
}
}
}
This is now a lot simpler with ServiceStack's new API Design released in (v3.9.15+).
#Rickard makes a lot of good points on how to re-structure your service so it's more REST-ful which is now easier to achieve with ServiceStack's new API which is now less restrictive and more flexible where the same service can handle multiple different Request DTOs and you're no longer restricted in the Response Type you can return.
Following the HTTP way you have to turn your way of thinking upside down. You need to think in terms of resources, i.e. users, friends, etc. Using HTTP you already have a finite set of methods, namely Get, Put, Post, and Delete.
Hence, the service API design could look like this:
RegisterUser() => POST /users
RemoveUser() => DELETE /users/{userid}
AddFriend() => POST /users/{userid}/friends
RemoveFriend() => DELETE /users/{userid}/friends/{friendid}
ModifyUser() => PUT /users/{userid}
etc.
To implement for example RemoveFriend in ServiceStack you could do like this:
public class UserFriendService : RestServiceBase<UserFriendRequest>
{
public override object OnPost(UserFriendRequest request)
{
// pseudo code
var user = GetUser(request.UserId);
var friend = GetUser(request.FriendId); // FriendId is a field in the HTTP body
user.Friends.Add(friend);
return HttpResult.Status201Created(user, ...);
}
//...
}
[Route("/users/{userId}/friends")]
public class UserFriendRequest
{
public string UserId { get; set; }
public string FriendId { get; set; }
}