.Net core 2.2 API versioning and proper routing - .net-core

I am creating an API. I use swagger but due to a huge number of controllers and actions, I want to split API endpoint by domain. To get this I thought about versioning of the API. I thought about using the Status of ApiVersion. The code of my controllers is below.
[ApiVersion("1.0-First")] //This is ApiVersion MajorVersion = 1, Status = "First"
[Route("api/v{version:apiVersion}/[controller]")]
public class FirstController
[ApiVersion("1.0-Second")]
[Route("api/v{version:apiVersion}/other")]
public class SecondController
My swagger looks fine and the definitions of parts of API are good. (I know that path should be without capital letters - this is for test purposes only)
But swagger can't reach any endpoint. Because the valid endpoint is at /api/v1.0-First/First not /api/v1/First.
My startUp class looks like below:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore().AddApiExplorer();
services.AddApiVersioning(c =>
{
c.ApiVersionReader = ApiVersionReader.Combine(
new QueryStringApiVersionReader("V"),
new UrlSegmentApiVersionReader());
c.ReportApiVersions = false;
c.DefaultApiVersion = new ApiVersion(1, 0);
});
services.AddVersionedApiExplorer(options =>
{
options.SubstituteApiVersionInUrl = true;
options.SubstitutionFormat = "V";
options.DefaultApiVersion = new ApiVersion(1, 0);
});
services.RegisterSwaggerConfiguration();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseDeveloperExceptionPage();
app.UseHttpsRedirection();
app.UseMvc();
app.AddSwagger(app.ApplicationServices.GetService<IApiVersionDescriptionProvider>(), Configuration);
}
There is some static class I wrote to add the dependencies based on IApiVersionDescriptionProvider
public static class SwaggerExtension
{
public static void RegisterSwaggerConfiguration(this IServiceCollection services)
{
services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
services.AddSwaggerGen();
}
public static void AddSwagger(this IApplicationBuilder app, IApiVersionDescriptionProvider provider, IConfiguration configuration)
{
var prefix = "swagger";
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.RoutePrefix = string.Empty;
foreach (var description in provider.ApiVersionDescriptions)
{
c.SwaggerEndpoint($"{prefix}/{description.GroupName}/swagger.json", description.GroupName);
}
});
}
}
And another class for SwaggerDoc generation.
public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
{
private readonly IApiVersionDescriptionProvider provider;
private readonly IConfiguration configuration;
public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider, IConfiguration configuration)
{
this.provider = provider;
this.configuration = configuration;
}
public void Configure(SwaggerGenOptions options)
{
foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description));
}
}
private OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description)
{
var info = new OpenApiInfo()
{
Title = description.GroupName,
Version = description.ApiVersion.ToString(),
};
if (description.IsDeprecated)
{
info.Description += " This API version has been deprecated.";
}
return info;
}
}
I want to get the routing work as api/v1/First or api/v1.0/First (this should not matter).
Maybe writting some custom middleware to handle this case would be good idea?
By now I am out of ideas and in general I couldn't find any articles about using status of ApiVersion.
EDIT:
Changed Title.

We had a similar problem some time ago. We needed to split an Api by a customer privilege/domain. The research took some time as well :), please note that we are using NSwag.
So as you already mentioned (custom middleware) we've created a custom OperationProcessor and used base type checking. Take a look at an example:
services.AddOpenApiDocument(document =>
{
document.Title = "API A";
document.OperationProcessors.Insert(0, new IncludeAApiControllersInSwagger());
});
services.AddOpenApiDocument(document =>
{
document.Title = "API B";
document.OperationProcessors.Insert(0, new IncludeBApiControllersInSwagger());
});
and then
private class IncludeAApiControllersInSwagger : IOperationProcessor
{
public bool Process(OperationProcessorContext context)
{
return IsControllerInType(context, typeof(AApiController));
}
}
private class IncludeBApiControllersInSwagger : IOperationProcessor
{
public bool Process(OperationProcessorContext context)
{
return IsControllerInType(context, typeof(BApiController));
}
}
The last step would be to build a proper inheritance over your controllers.

An API version is always an API version; the values are explicit - by design. There is no universe where 1.0-First can map to an API, but not include the status.
The status is most useful for pre-releases. For example, you might have /first?api-version=1.0-preview.1. When you have a volatile, preview version of an API, this prevents you from having to bump up to 1.1 and so on. 1.0 is greater than 1.0-preview.1.
From your description, it sounds like you want to group or categorize your APIs by an additional level. The Swagger UI only supports a single level of grouping, but ASP.NET API Versioning 7.0+ now has support to make custom group names with API versions easy to configure using the FormatGroupName option.
If your API has a custom group name like this:
[ApiVersion(1.0)]
[ApiExplorerSettings(GroupName = "First")]
[Route("api/v{version:apiVersion}/[controller]")]
public class FirstController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok();
}
You can now configure the combination of both like this:
services.AddApiVersioning()
.AddApiExplorer(
options =>
{
options.SubstituteApiVersionInUrl = true;
options.FormatGroupName = (group, version) => $"{version}-{group}";
});
This only works if you set a custom group name and define a callback. The rules are:
Default configuration; formatted ApiVersion
Group name set, but not callback; use group name
Group name and callback set; result for callback with group and formatted ApiVersion
Only callback set; ignored and uses default configuration as there's no group name
The ApiVersion is formatted according to GroupNameFormat. By default, this will simply be ApiVersion.ToString(). You can still use it if you want to. For example, if GroupNameFormat = "'v'VVV";, then the formatted name via the callback will result in v1-First.
Despite all of this configuration and grouping, the route to your API will still be: api/v1/first. I believe that will get you both of your goals.

Related

How to convert to Xunit using mocking

I have these two methods in my service class
public class PatientService : IPatientService
{
private readonly IRestClient _restClient;
private readonly IAppSettings _appSettings;
public PatientService(IRestClient restClient, IAppSettings appSettings)
{
_restClient = restClient;
_appSettings = appSettings;
}
public async Task<IList<PatientViewModel>> GetPatients(int wardId)
{
var url = _appSettings.Server + _appSettings.PatientServiceEndPoint + wardId;
var token = _appSettings.Token;
return GetPatientList(await _restClient.GetAsync<List<PatientInfo>>(url, token));
}
public IList<PatientViewModel> GetPatientList(IList<PatientInfo> patientInfoList)
{
return patientInfoList.Select(p => new PatientViewModel(p)).ToList();
}
}
I need to add this code to my Xunit.cs. How to do it?
I've implemented this and I do not know how to proceed.
private readonly PatientListPageViewModel _patientListPageViewModel;
private readonly Mock<IPatientService> _patient;
public PatientServiceTests()
{
_patient = new Mock<IPatientService>();
_patientListPageViewModel = new PatientListPageViewModel(_patient.Object);
}
[Fact]
public void GetListByWard_PassingWardId_GetPatientsCountAccordingToWardId()
{
}
This is what I tried to do. How to convert those two methods in service to be testable?
You did get mocking a bit wrong. It is not the component under test that is mocked, but its dependencies. When unit-testing you'd like to test a unit in isolation. Your case of mocking would be kind of correct if you unit-tested the PatientListPageViewModel, but since your test class is named PatientServiceTests I assume that you really wanted to test PatientService. If you wanted to test the former, you would be quite right to mock IPatientService, but when testing PatientService, IRestClient and IAppSettings shall be mocked
public PatientServiceTests()
{
_restClientMock = new Mock<IRestClient>();
_appSettingsMock = new Mock<IAppSettings>();
_patientService = new PatientService(_restClientMock.Object, _appSettingsMock.Object);
}
And your test could be something like
[Fact]
public async Task ReturnsCorrectPatientList() // async supported as of xUnit 1.9
{
// set up the mock
_restClientMock.SetUp(restClient => restClient.GetAsync<List<Patient>>(It.IsAny<string>(), It.IsAny<string>())
.Returns(() => Task.FromResult(/* what patients it shall return */));
var result = await _patientService.GetPatients(0);
// compare whether the returned result matches your expectations
}
If you wanted to test whether the URL is formed correctly, you could use Verify
[Theory]
[InlineData("SERVER", "ENDPOINT", 12, "1234", "SERVERENDPOINT12")]
[InlineData("https://localhost:65000", "/patients/", 5, https://localhost:65000/patients/5")]
public void TestWhetherCorrectUrlIsCalled(string server, string endpoint, int wardId, string token, string expectedUrl)
{
_appSettingsMock.SetupGet(appSettings => appSettings.Server).Returns(server);
_appSettingsMock.SetupGet(appSettings => appSettings.PatientServiceEndPoint).Returns(endpoint);
_appSettingsMock.SetupGet(appSettings => appSettings.Token).Returns(token);
_restClientMock.SetUp(restClient => restClient.GetAsync<List<Patient>>(It.IsAny<string>(), It.IsAny<string>())
.Returns(() => Task.FromResult(new List<Patient>()));
// we do not need the result
await _patientService.GetPatients(wardId);
_restClientMock.Verify(restClient => restClient.GetAsync<List<Patient>>(exptectedUrl, token), Times.Once);
}
We are setting up the IRestClient in this case, since it would return null otherwise. And await null would cause your test to fail. After GetPatients has been called we are using Verify to verify that GetAsync has been called with the correct parameters. If it has not been called, Verify will throw and your test will fail. Times.Once means, that GetAsync shall have been called once and only once.
On a side note: Viewmodels shall have a meaning in the context of your user interface only. Services shall be independent and hence not return viewmodels, as you did, but POCOs (or maybe domain models). In this case the interface of your service should be
public interface IPatientService
{
public async Task<IList<Patient>> GetPatients(int wardId);
// ...
}

Asp.Net Core Set Default API Versioning

I'm using Asp.Net Core as a Rest API Service. I need to have API Versioning. Actually, I set in the Startup following settings and It's work correctly but when I set to default Version It's not working on.
services.AddVersionedApiExplorer(
options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
options.AssumeDefaultVersionWhenUnspecified = true;
options.DefaultApiVersion = new ApiVersion(1, 0);
});
services.AddApiVersioning(
options =>
{
options.ReportApiVersions = true;
options.AssumeDefaultVersionWhenUnspecified = true;
options.DefaultApiVersion = new ApiVersion(1, 0);
})
.AddMvc(
options =>
{
options.RespectBrowserAcceptHeader = true;
})
.AddXmlSerializerFormatters();
and set Attribute in Controllers like this:
Version 1:
[ApiController]
[Route("v{version:apiVersion}/[controller]")]
[ApiVersion("1.0")]
public class UsersController : ControllerBase
{
[HttpGet("log")]
public string Get()
{
return $"{DateTime.Now}";
}
}
Version 2:
[ApiController]
[Route("v{version:apiVersion}/[controller]")]
[ApiVersion("2.0")]
public class UsersController : ControllerBase
{
[HttpGet("log")]
public string Get()
{
return $"{DateTime.Now}";
}
}
I can get the result as fllowing urls:
http://localhost:5000/v1/users/log => Status Code: 200
http://localhost:5000/v2/users/log => Status Code: 200
But http://localhost:5000/users/log => Status Code: 404
How I can set the default API in Versioning?
Thanks, everyone for taking the time to try and help explain
Your configuration correctly sets default api version if none is specified. But your routes requires request url to include v{version} part. So one possible solution is to add another route without v{version} like this
[ApiController]
[Route("v{version:apiVersion}/[controller]")]
[Route("/[controller]")]
[ApiVersion("1.0")]
public class UsersController : ControllerBase
{
[HttpGet("log")]
public string Get()
{
return $"{DateTime.Now}";
}
}
Now request to http://localhost:5000/users/log will point to controller with api version 1.0. If you add this route to second controller the version 1.0 will still be picked because default ApiVersion will be selected and it's 1.0.

Spring OAuth2 Making `state` param at least 32 characters long

I am attempting to authorize against an external identity provider. Everything seems setup fine, but I keep getting a validation error with my identity provider because the state parameter automatically tacked onto my authorization request is not long enough:
For example:
&state=uYG5DC
The requirements of my IDP say that this state param must be at least 32-characters long. How can I programmatically increase the size of this auto-generated number?
Even if I could generate this number myself, it is not possible to override with other methods I have seen suggested. The following attempt fails because my manual setting of ?state=abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz is superceded by the autogenerated param placed after it during the actual request:
#Bean
public OAuth2ProtectedResourceDetails loginGovOpenId() {
AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails() {
#Override
public String getUserAuthorizationUri() {
return super.getUserAuthorizationUri() + "?state=abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz";
}
};
details.setClientId(clientId);
details.setAccessTokenUri(accessTokenUri);
details.setUserAuthorizationUri(userAuthorizationUri);
details.setScope(Arrays.asList("openid", "email"));
details.setPreEstablishedRedirectUri(redirectUri);
details.setUseCurrentUri(true);
return details;
}
The 6-character setting seems to be set here, is there a way to override this?
https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/common/util/RandomValueStringGenerator.java
With the help of this post:
spring security StateKeyGenerator custom instance
I was able to come up with a working solution.
In my configuration class marked with these annotations:
#Configuration
#EnableOAuth2Client
I configured the following beans:
#Bean
public OAuth2ProtectedResourceDetails loginGovOpenId() {
AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
AuthorizationCodeResourceDetails details = new
details.setClientId(clientId);
details.setClientSecret(clientSecret);
details.setAccessTokenUri(accessTokenUri);
details.setUserAuthorizationUri(userAuthorizationUri);
details.setScope(Arrays.asList("openid", "email"));
details.setPreEstablishedRedirectUri(redirectUri);
details.setUseCurrentUri(true);
return details;
}
#Bean
public StateKeyGenerator stateKeyGenerator() {
return new CustomStateKeyGenerator();
}
#Bean
public AccessTokenProvider accessTokenProvider() {
AuthorizationCodeAccessTokenProvider accessTokenProvider = new AuthorizationCodeAccessTokenProvider();
accessTokenProvider.setStateKeyGenerator(stateKeyGenerator());
return accessTokenProvider;
}
#Bean
public OAuth2RestTemplate loginGovOpenIdTemplate(final OAuth2ClientContext clientContext) {
final OAuth2RestTemplate template = new OAuth2RestTemplate(loginGovOpenId(), clientContext);
template.setAccessTokenProvider(accessTokenProvider());
return template;
}
Where my CustomStateKeyGenerator implementation class looks as follows:
public class CustomStateKeyGenerator implements StateKeyGenerator {
// login.gov requires state to be at least 32-characters long
private static int length = 32;
private RandomValueStringGenerator generator = new RandomValueStringGenerator(length);
#Override
public String generateKey(OAuth2ProtectedResourceDetails resource) {
return generator.generate();
}
}

Application Insights in IHostedService console application

I am trying to enable Application Insights in a console application using IHostedService (for the moment, it's a simple console application which we run as WebJob, in future in containers).
As far as my knowledge goes, in the following code, so far we do not have any extension to register globally Application Insights as an implementation of ILogger:
public static class Program
{
public static Task Main(string[] args)
{
var hostBuilder = new HostBuilder()
.ConfigureHostConfiguration(config =>
{
config.SetBasePath(Directory.GetCurrentDirectory());
config.AddJsonFile("appsettings.json", optional: false);
config.AddEnvironmentVariables();
})
.ConfigureLogging((context, logging) =>
{
logging.AddConfiguration(context.Configuration.GetSection("Logging"));
if (context.HostingEnvironment.IsDevelopment())
{
logging.AddConsole();
}
else
{
//TODO: register ApplicationInsights
}
});
return hostBuilder.RunConsoleAsync();
}
}
So far, I found out that potentially, I should be able to set everything up using custom implementation of the logger, i.e. public class ApplicationInsightsLogger : ILogger, and then... register it in the container so that DI resolves it.
Is this the right direction?
I made an extension that I could use from either an IHost or an IWebHost:
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.ApplicationInsights;
public static class LoggingBuilderExtensions
{
public static ILoggingBuilder AddLogging(this ILoggingBuilder loggingBuilder)
{
loggingBuilder.AddFilter<ApplicationInsightsLoggerProvider>("", LogLevel.Trace);
loggingBuilder.AddAzureWebAppDiagnostics();
loggingBuilder.AddApplicationInsights();
return loggingBuilder;
}
}
Since I'm not sending in the context (HostBuilderContext or WebHostBuilderContext), I can use it in either app type like this:
new HostBuilder().ConfigureLogging(loggingBuilder => loggingBuilder.AddLogging())
or
WebHost.CreateDefaultBuilder().ConfigureLogging(loggingBuilder => loggingBuilder.AddLogging())
If you needed a specific property from the context (like environment type), you could extract that and send it in as a parameter to the extension.
Here's a reference: https://github.com/Microsoft/ApplicationInsights-dotnet-logging/blob/develop/src/ILogger/Readme.md

Override host of webapi odata links

I'm using WebAPI 2.2 and Microsoft.AspNet.OData 5.7.0 to create an OData service that supports paging.
When hosted in the production environment, the WebAPI lives on a server that is not exposed externally, hence the various links returned in the OData response such as the #odata.context and #odata.nextLink point to the internal IP address e.g. http://192.168.X.X/<AccountName>/api/... etc.
I've been able to modify the Request.ODataProperties().NextLink by implementing some logic in each and every ODataController method to replace the internal URL with an external URL like https://account-name.domain.com/api/..., but this is very inconvenient and it only fixes the NextLinks.
Is there some way to set an external host name at configuration time of the OData service? I've seen a property Request.ODataProperties().Path and wonder if it's possible to set a base path at the config.MapODataServiceRoute("odata", "odata", GetModel()); call, or in the GetModel() implementation using for instance the ODataConventionModelBuilder?
UPDATE: The best solution I've come up with so far, is to create a BaseODataController that overrides the Initialize method and checks whether the Request.RequestUri.Host.StartsWith("beginning-of-known-internal-IP-address") and then do a RequestUri rewrite like so:
var externalAddress = ConfigClient.Get().ExternalAddress; // e.g. https://account-name.domain.com
var account = ConfigClient.Get().Id; // e.g. AccountName
var uriToReplace = new Uri(new Uri("http://" + Request.RequestUri.Host), account);
string originalUri = Request.RequestUri.AbsoluteUri;
Request.RequestUri = new Uri(Request.RequestUri.AbsoluteUri.Replace(uriToReplace.AbsoluteUri, externalAddress));
string newUri = Request.RequestUri.AbsoluteUri;
this.GetLogger().Info($"Request URI was rewritten from {originalUri} to {newUri}");
This perfectly fixes the #odata.nextLink URLs for all controllers, but for some reason the #odata.context URLs still get the AccountName part (e.g. https://account-name.domain.com/AccountName/api/odata/$metadata#ControllerName) so they still don't work.
Rewriting the RequestUri is sufficient to affect #odata.nextLink values because the code that computes the next link depends on the RequestUri directly. The other #odata.xxx links are computed via a UrlHelper, which is somehow referencing the path from the original request URI. (Hence the AccountName you see in your #odata.context link. I've seen this behavior in my code, but I haven't been able to track down the source of the cached URI path.)
Rather than rewrite the RequestUri, we can solve the problem by creating a CustomUrlHelper class to rewrite OData links on the fly. The new GetNextPageLink method will handle #odata.nextLink rewrites, and the Link method override will handle all other rewrites.
public class CustomUrlHelper : System.Web.Http.Routing.UrlHelper
{
public CustomUrlHelper(HttpRequestMessage request) : base(request)
{ }
// Change these strings to suit your specific needs.
private static readonly string ODataRouteName = "ODataRoute"; // Must be the same as used in api config
private static readonly string TargetPrefix = "http://localhost:8080/somePathPrefix";
private static readonly int TargetPrefixLength = TargetPrefix.Length;
private static readonly string ReplacementPrefix = "http://www.contoso.com"; // Do not end with slash
// Helper method.
protected string ReplaceTargetPrefix(string link)
{
if (link.StartsWith(TargetPrefix))
{
if (link.Length == TargetPrefixLength)
{
link = ReplacementPrefix;
}
else if (link[TargetPrefixLength] == '/')
{
link = ReplacementPrefix + link.Substring(TargetPrefixLength);
}
}
return link;
}
public override string Link(string routeName, IDictionary<string, object> routeValues)
{
var link = base.Link(routeName, routeValues);
if (routeName == ODataRouteName)
{
link = this.ReplaceTargetPrefix(link);
}
return link;
}
public Uri GetNextPageLink(int pageSize)
{
return new Uri(this.ReplaceTargetPrefix(this.Request.GetNextPageLink(pageSize).ToString()));
}
}
Wire-up the CustomUrlHelper in the Initialize method of a base controller class.
public abstract class BaseODataController : ODataController
{
protected abstract int DefaultPageSize { get; }
protected override void Initialize(System.Web.Http.Controllers.HttpControllerContext controllerContext)
{
base.Initialize(controllerContext);
var helper = new CustomUrlHelper(controllerContext.Request);
controllerContext.RequestContext.Url = helper;
controllerContext.Request.ODataProperties().NextLink = helper.GetNextPageLink(this.DefaultPageSize);
}
Note in the above that the page size will be the same for all actions in a given controller class. You can work around this limitation by moving the assignment of ODataProperties().NextLink to the body of a specific action method as follows:
var helper = this.RequestContext.Url as CustomUrlHelper;
this.Request.ODataProperties().NextLink = helper.GetNextPageLink(otherPageSize);
The answer by lencharest is promising, but I found an improvement on his method. Rather than using the UrlHelper, I created a class derived from System.Net.Http.DelegatingHandler. This class is inserted (first) into the message handling pipeline and thus has a crack at altering the incoming HttpRequestMessage. It's an improvement over the above solution because in addition to altering the controller-specific URLs (as the UrlHelper does, e,g, https://data.contoso.com/odata/MyController), it also alters the url that appears as the xml:base in the OData service document (e.g., https://data.contoso.com/odata).
My particular application was to host an OData service behind a proxy server, and I wanted all the URLs presented by the server to be the externally-visible URLs, not the internally-visible ones. And, I didn't want to have to rely on annotations for this; I wanted it to be fully automatic.
The message handler looks like this:
public class BehindProxyMessageHandler : DelegatingHandler
{
protected async override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
var builder = new UriBuilder(request.RequestUri);
var visibleHost = builder.Host;
var visibleScheme = builder.Scheme;
var visiblePort = builder.Port;
if (request.Headers.Contains("X-Forwarded-Host"))
{
string[] forwardedHosts = request.Headers.GetValues("X-Forwarded-Host").First().Split(new char[] { ',' });
visibleHost = forwardedHosts[0].Trim();
}
if (request.Headers.Contains("X-Forwarded-Proto"))
{
visibleScheme = request.Headers.GetValues("X-Forwarded-Proto").First();
}
if (request.Headers.Contains("X-Forwarded-Port"))
{
try
{
visiblePort = int.Parse(request.Headers.GetValues("X-Forwarded-Port").First());
}
catch (Exception)
{ }
}
builder.Host = visibleHost;
builder.Scheme = visibleScheme;
builder.Port = visiblePort;
request.RequestUri = builder.Uri;
var response = await base.SendAsync(request, cancellationToken);
return response;
}
}
You wire the handler up in WebApiConfig.cs:
config.Routes.MapODataServiceRoute(
routeName: "odata",
routePrefix: "odata",
model: builder.GetEdmModel(),
pathHandler: new DefaultODataPathHandler(),
routingConventions: ODataRoutingConventions.CreateDefault()
);
config.MessageHandlers.Insert(0, new BehindProxyMessageHandler());
There is another solution, but it overrides url for the entire context.
What I'd like to suggest is:
Create owin middleware and override Host and Scheme properties inside
Register the middleware as the first one
Here is an example of middleware
public class RewriteUrlMiddleware : OwinMiddleware
{
public RewriteUrlMiddleware(OwinMiddleware next)
: base(next)
{
}
public override async Task Invoke(IOwinContext context)
{
context.Request.Host = new HostString(Settings.Default.ProxyHost);
context.Request.Scheme = Settings.Default.ProxyScheme;
await Next.Invoke(context);
}
}
ProxyHost is the host you want to have. Example: test.com
ProxyScheme is the scheme you want: Example: https
Example of middleware registration
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.Use(typeof(RewriteUrlMiddleware));
var config = new HttpConfiguration();
WebApiConfig.Register(config);
app.UseWebApi(config);
}
}
A couple of years later, using ASP.NET Core, I figured that the easiest way to apply it in my service was to just create a filter that masquerades the host name. (AppConfig is a custom configuration class that contains the host name, among other things.)
public class MasqueradeHostFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
var appConfig = context.HttpContext.RequestServices.GetService<AppConfig>();
if (!string.IsNullOrEmpty(appConfig?.MasqueradeHost))
context.HttpContext.Request.Host = new HostString(appConfig.MasqueradeHost);
}
}
Apply the filter to the controller base class.
[MasqueradeHostFilter]
public class AppODataController : ODataController
{
}
The result is a nicely formatted output:
{ "#odata.context":"https://app.example.com/odata/$metadata" }
Just my two cents.
Using system.web.odata 6.0.0.0.
Setting the NextLink property too soon is problematic. Every reply will then have a nextLink in it. The last page should of course be free of such decorations.
http://docs.oasis-open.org/odata/odata-json-format/v4.0/os/odata-json-format-v4.0-os.html#_Toc372793048 says:
URLs present in a payload (whether request or response) MAY be
represented as relative URLs.
One way that I hope will work is to override EnableQueryAttribute:
public class myEnableQueryAttribute : EnableQueryAttribute
{
public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
{
var result = base.ApplyQuery(queryable, queryOptions);
var nextlink = queryOptions.Request.ODataProperties().NextLink;
if (nextlink != null)
queryOptions.Request.ODataProperties().NextLink = queryOptions.Request.RequestUri.MakeRelativeUri(nextlink);
return result;
}
}
ApplyQuery() is where the "overflow" is detected. It basically asks for pagesize+1 rows and will set NextLink if the result set contains more than pagesize rows.
At this point it is relatively easy to rewrite NextLink to a relative URL.
The downside is that every odata method must now be adorned with the new myEnableQuery attribute:
[myEnableQuery]
public async Task<IHttpActionResult> Get(ODataQueryOptions<TElement> options)
{
...
}
and other URLs embedded elsewhere remains problematic. odata.context remains a problem. I want to avoid playing with the request URL, because I fail to see how that is maintainable over time.
Your question boils down to controlling the service root URI from within the service itself. My first thought was to look for a hook on the media type formatters used to serialize responses. ODataMediaTypeFormatter.MessageWriterSettings.PayloadBaseUri and ODataMediaTypeFormatter.MessageWriterSettings.ODataUri.ServiceRoot are both settable properties that suggest a solution. Unfortunately, ODataMediaTypeFormatter resets these properties on every call to WriteToStreamAsync.
The work-around is not obvious, but if you dig through the source code you'll eventually reach a call to IODataPathHandler.Link. A path handler is an OData extension point, so you can create a custom path handler that always returns an absolute URI which begins with the service root you desire.
public class CustomPathHandler : DefaultODataPathHandler
{
private const string ServiceRoot = "http://example.com/";
public override string Link(ODataPath path)
{
return ServiceRoot + base.Link(path);
}
}
And then register that path handler during service configuration.
// config is an instance of HttpConfiguration
config.MapODataServiceRoute(
routeName: "ODataRoute",
routePrefix: null,
model: builder.GetEdmModel(),
pathHandler: new CustomPathHandler(),
routingConventions: ODataRoutingConventions.CreateDefault()
);

Resources