I'm attempting to serve video files from ASP.NET MVC to iPhone clients. The video is formatted properly, and if I have it in a publicly accessible web directory it works fine.
The core issue from what I've read is that the iPhone requires you to have a resume-ready download environment that lets you filter your byte ranges through HTTP headers. I assume this is so that users can skip forward through videos.
When serving files with MVC, these headers do not exist. I've tried to emulate it, but with no luck. We have IIS6 here and I'm unable to do many header manipulations at all. ASP.NET will complain at me saying "This operation requires IIS integrated pipeline mode."
Upgrading isn't an option, and I'm not allowed to move the files to a public web share. I feel limited by our environment but I'm looking for solutions nonetheless.
Here is some sample code of what I'm trying to do in short...
public ActionResult Mobile(string guid = "x")
{
guid = Path.GetFileNameWithoutExtension(guid);
apMedia media = DB.apMedia_GetMediaByFilename(guid);
string mediaPath = Path.Combine(Transcode.Swap_MobileDirectory, guid + ".m4v");
if (!Directory.Exists(Transcode.Swap_MobileDirectory)) //Make sure it's there...
Directory.CreateDirectory(Transcode.Swap_MobileDirectory);
if(System.IO.File.Exists(mediaPath))
return base.File(mediaPath, "video/x-m4v");
return Redirect("~/Error/404");
}
I know that I need to do something like this, however I'm unable to do it in .NET MVC. http://dotnetslackers.com/articles/aspnet/Range-Specific-Requests-in-ASP-NET.aspx
Here is an example of an HTTP response header that works:
Date Mon, 08 Nov 2010 17:02:38 GMT
Server Apache
Last-Modified Mon, 08 Nov 2010 17:02:13 GMT
Etag "14e78b2-295eff-4cd82d15"
Accept-Ranges bytes
Content-Length 2711295
Content-Range bytes 0-2711294/2711295
Keep-Alive timeout=15, max=100
Connection Keep-Alive
Content-Type text/plain
And here is an example of one that doesn't (this is from .NET)
Server ASP.NET Development Server/10.0.0.0
Date Mon, 08 Nov 2010 18:26:17 GMT
X-AspNet-Version 4.0.30319
X-AspNetMvc-Version 2.0
Content-Range bytes 0-2711294/2711295
Cache-Control private
Content-Type video/x-m4v
Content-Length 2711295
Connection Close
Any ideas? Thank you.
UPDATE: This is now a project on CodePlex.
Okay, I got it working on my local testing station and I can stream videos to my iPad. It's a bit dirty because it was a little more difficult than I expected and now that it's working I don't have the time to clean it up at the moment. Key parts:
Action Filter:
public class ByteRangeRequest : FilterAttribute, IActionFilter
{
protected string RangeStart { get; set; }
protected string RangeEnd { get; set; }
public ByteRangeRequest(string RangeStartParameter, string RangeEndParameter)
{
RangeStart = RangeStartParameter;
RangeEnd = RangeEndParameter;
}
public void OnActionExecuting(ActionExecutingContext filterContext)
{
if (filterContext == null)
throw new ArgumentNullException("filterContext");
if (!filterContext.ActionParameters.ContainsKey(RangeStart))
filterContext.ActionParameters.Add(RangeStart, null);
if (!filterContext.ActionParameters.ContainsKey(RangeEnd))
filterContext.ActionParameters.Add(RangeEnd, null);
var headerKeys = filterContext.RequestContext.HttpContext.Request.Headers.AllKeys.Where(key => key.Equals("Range", StringComparison.InvariantCultureIgnoreCase));
Regex rangeParser = new Regex(#"(\d+)-(\d+)", RegexOptions.Compiled);
foreach(string headerKey in headerKeys)
{
string value = filterContext.RequestContext.HttpContext.Request.Headers[headerKey];
if (!string.IsNullOrEmpty(value))
{
if (rangeParser.IsMatch(value))
{
Match match = rangeParser.Match(value);
filterContext.ActionParameters[RangeStart] = int.Parse(match.Groups[1].ToString());
filterContext.ActionParameters[RangeEnd] = int.Parse(match.Groups[2].ToString());
break;
}
}
}
}
public void OnActionExecuted(ActionExecutedContext filterContext)
{
}
}
Custom Result based on FileStreamResult:
public class ContentRangeResult : FileStreamResult
{
public int StartIndex { get; set; }
public int EndIndex { get; set; }
public long TotalSize { get; set; }
public DateTime LastModified { get; set; }
public FileStreamResult(int startIndex, int endIndex, long totalSize, DateTime lastModified, string contentType, Stream fileStream)
: base(fileStream, contentType)
{
StartIndex = startIndex;
EndIndex = endIndex;
TotalSize = totalSize;
LastModified = lastModified;
}
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
throw new ArgumentNullException("context");
HttpResponseBase response = context.HttpContext.Response;
response.ContentType = this.ContentType;
response.AddHeader(HttpWorkerRequest.GetKnownResponseHeaderName(HttpWorkerRequest.HeaderContentRange), string.Format("bytes {0}-{1}/{2}", StartIndex, EndIndex, TotalSize));
response.StatusCode = 206;
WriteFile(response);
}
protected override void WriteFile(HttpResponseBase response)
{
Stream outputStream = response.OutputStream;
using (this.FileStream)
{
byte[] buffer = new byte[0x1000];
int totalToSend = EndIndex - StartIndex;
int bytesRemaining = totalToSend;
int count = 0;
FileStream.Seek(StartIndex, SeekOrigin.Begin);
while (bytesRemaining > 0)
{
if (bytesRemaining <= buffer.Length)
count = FileStream.Read(buffer, 0, bytesRemaining);
else
count = FileStream.Read(buffer, 0, buffer.Length);
outputStream.Write(buffer, 0, count);
bytesRemaining -= count;
}
}
}
}
My MVC action:
[ByteRangeRequest("StartByte", "EndByte")]
public FileStreamResult NextSegment(int? StartByte, int? EndByte)
{
FileStream contentFileStream = System.IO.File.OpenRead(#"C:\temp\Gets.mp4");
var time = System.IO.File.GetLastWriteTime(#"C:\temp\Gets.mp4");
if (StartByte.HasValue && EndByte.HasValue)
return new ContentRangeResult(StartByte.Value, EndByte.Value, contentFileStream.Length, time, "video/x-m4v", contentFileStream);
return new ContentRangeResult(0, (int)contentFileStream.Length, contentFileStream.Length, time, "video/x-m4v", contentFileStream);
}
I really hope this helps. I spent a LOT of time on this! One thing you might want to try is removing pieces until it breaks again. It would be nice to see if the ETag stuff, modified date, etc. could be removed. I just don't have the time at the moment.
Happy coding!
I tried looking for an existing extension but I didn't immediately find one (maybe my search-fu is weak.)
My immediate thought is that you'll need to make two new classes.
First, create a class inheriting from ActionMethodSelectorAttribute. This is the same base class for HttpGet, HttpPost, etc. In this class you'll override IsValidForRequest. In that method, examine the headers to see if a range was requested. You can now use this attribute to decorate a method in your controller which will get called when someone is requested part of a stream (iOS, Silverlight, etc.)
Second, create a class inheriting from either ActionResult or maybe FileResult and override the ExecuteResult method to add the headers you identified for the byte range that you'll be returning. Return it like you would a JSON object with parameters for the byte range start, end, total size so it can generate the response headers correctly.
Take a look at the way FileContentResult is implemented to see how you access the context's HttpResponse object to alter the headers.
Take a look at HttpGet to see how it implements the check for IsValidForRequest. The source is available on CodePlex or you can use Reflector like I just did.
You might use this info to do a little more searching and see if anyone has already created this custom ActionResult already.
For reference, here is what the AcceptVerbs attribute looks like:
public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
string httpMethodOverride = controllerContext.HttpContext.Request.GetHttpMethodOverride();
return this.Verbs.Contains<string>(httpMethodOverride, StringComparer.OrdinalIgnoreCase);
}
And here is what FileResult looks like. Notice the use of AddHeader:
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
HttpResponseBase response = context.HttpContext.Response;
response.ContentType = this.ContentType;
if (!string.IsNullOrEmpty(this.FileDownloadName))
{
string headerValue = ContentDispositionUtil.GetHeaderValue(this.FileDownloadName);
context.HttpContext.Response.AddHeader("Content-Disposition", headerValue);
}
this.WriteFile(response);
}
I just pieced this together. I don't know if it will suit your needs (or works).
public class ContentRangeResult : FileStreamResult
{
public int StartIndex { get; set; }
public int EndIndex { get; set; }
public int TotalSize { get; set; }
public ContentRangeResult(int startIndex, int endIndex, string contentType, Stream fileStream)
:base(fileStream, contentType)
{
StartIndex = startIndex;
EndIndex = endIndex;
TotalSize = endIndex - startIndex;
}
public ContentRangeResult(int startIndex, int endIndex, string contentType, string fileDownloadName, Stream fileStream)
: base(fileStream, contentType)
{
StartIndex = startIndex;
EndIndex = endIndex;
TotalSize = endIndex - startIndex;
FileDownloadName = fileDownloadName;
}
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
HttpResponseBase response = context.HttpContext.Response;
if (!string.IsNullOrEmpty(this.FileDownloadName))
{
System.Net.Mime.ContentDisposition cd = new System.Net.Mime.ContentDisposition() { FileName = FileDownloadName };
context.HttpContext.Response.AddHeader("Content-Disposition", cd.ToString());
}
context.HttpContext.Response.AddHeader("Accept-Ranges", "bytes");
context.HttpContext.Response.AddHeader("Content-Range", string.Format("bytes {0}-{1}/{2}", StartIndex, EndIndex, TotalSize));
//Any other headers?
this.WriteFile(response);
}
protected override void WriteFile(HttpResponseBase response)
{
Stream outputStream = response.OutputStream;
using (this.FileStream)
{
byte[] buffer = new byte[0x1000];
int totalToSend = EndIndex - StartIndex;
int bytesRemaining = totalToSend;
int count = 0;
while (bytesRemaining > 0)
{
if (bytesRemaining <= buffer.Length)
count = FileStream.Read(buffer, 0, bytesRemaining);
else
count = FileStream.Read(buffer, 0, buffer.Length);
outputStream.Write(buffer, 0, count);
bytesRemaining -= count;
}
}
}
}
Use it like this:
return new ContentRangeResult(50, 100, "video/x-m4v", "SomeOptionalFileName", contentFileStream);
Can you move outside of MVC? This is a case where the system abstractions are shooting you in the foot, but a plain jane IHttpHandler should have alot more options.
All that said, before you implement your own streaming server, you are probably better off buying or renting one . . .
The header that work have the Content-type set to text/plain, is that correct or is a typo?.
Anyone, you can try to set this headers on the Action with:
Response.Headers.Add(...)
Related
Hi guys
I'm trying to get some "hardcoded" values from an apex Method, but when I write a console.log it's comming empty.
Here's the code I'm working on:
#wire(getValues)
wiredValues({error, data})
if(data) {
console.log("Data::::::",data);
this.getVal = JSON.stringify(data);
} else if(error){
this.error = error;
this.getVal = undefined;
console.log("No values");
}
Here's my apex method(I'm trying to make kind of "fake" callout but i'm not sure if im right):
public without sharing class getSomeValues {
#AuraEnabled(cacheable = true)
public static List<wrapVal> getWrapVal() {
HttpResponse request = new HttpResponse();
request.setBody('{"Values": ["1000", "2000", "3000", "4000", "5000"]}');
Map<String, Object> results = (Map<String, Object>)
JSON.deserializeUntyped(request.getBody());
List<Object> sumVal = (List<Object>) results.get('Values');
List<wrapVal> newLstValues = new List<wrapVal>();
for (Object getValues : sumVal ) {
wrapVal newLstValue = new wrapVal();
newLstValue.nwValue = String.valueOf(getValues);
newLstValues.add(newLstValue);
System.debug("getValues::::::"+ newLstValue.nwValue);
}
return newLstValues;
}
public class wrapVal {
public String nwValue { get; set; }
}
Debug:
So idk what i'm doing wrong, if can share with me some advice or documentation it'd be great.
Thanks
It seems like you need to decorate the properties of the class wrapVal with #AuraEnabled. Otherwise LWC won't receive them.
public class wrapVal {
#AuraEnabled
public String nwValue { get; set; }
}
I'm upgrading some legacy to target Android Q, and of course this code stop working:
String[] PROJECTION_BUCKET = {MediaStore.Images.ImageColumns.BUCKET_ID,
MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME,
MediaStore.Images.ImageColumns.DATE_TAKEN,
MediaStore.Images.ImageColumns.DATA,
"COUNT(" + MediaStore.Images.ImageColumns._ID + ") AS COUNT",
MediaStore.Files.FileColumns.MEDIA_TYPE,
MediaStore.MediaColumns._ID};
String BUCKET_GROUP_BY = " 1) and " + BUCKET_WHERE.toString() + " GROUP BY 1,(2";
cur = context.getContentResolver().query(images, PROJECTION_BUCKET,
BUCKET_GROUP_BY, null, BUCKET_ORDER_BY);
android.database.sqlite.SQLiteException: near "GROUP": syntax error (code 1 SQLITE_ERROR[1])
Here it supposed to obtain list of images with album name, date, count of pictures - one image for each album, so we can create album picker screen without querying all pictures and loop through it to create albums.
Is it possible to group query results with contentResolver since SQL queries stoped work?
(I know that ImageColumns.DATA and "COUNT() AS COUNT" are deprecated too, but this is a question about GROUP BY)
(There is a way to query albums and separately query photo, to obtain photo uri for album cover, but i want to avoid overheads)
Unfortunately Group By is no longer supported in Android 10 and above, neither any aggregated functions such as COUNT. This is by design and there is no workaround.
The solution is what you are actually trying to avoid, which is to query, iterate, and get metrics.
To get you started you can use the next snipped, which will resolve the buckets (albums), and the amount of records in each one.
I haven't added code to resolve the thumbnails, but is easy. You must perform a query for each bucket Id from all the Album instances, and use the image from the first record.
public final class AlbumQuery
{
#NonNull
public static HashMap<String, AlbumQuery.Album> get(#NonNull final Context context)
{
final HashMap<String, AlbumQuery.Album> output = new HashMap<>();
final Uri contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
final String[] projection = {MediaStore.Images.Media.BUCKET_DISPLAY_NAME, MediaStore.Images.Media.BUCKET_ID};
try (final Cursor cursor = context.getContentResolver().query(contentUri, projection, null, null, null))
{
if ((cursor != null) && (cursor.moveToFirst() == true))
{
final int columnBucketName = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.BUCKET_DISPLAY_NAME);
final int columnBucketId = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.BUCKET_ID);
do
{
final String bucketId = cursor.getString(columnBucketId);
final String bucketName = cursor.getString(columnBucketName);
if (output.containsKey(bucketId) == false)
{
final int count = AlbumQuery.getCount(context, contentUri, bucketId);
final AlbumQuery.Album album = new AlbumQuery.Album(bucketId, bucketName, count);
output.put(bucketId, album);
}
} while (cursor.moveToNext());
}
}
return output;
}
private static int getCount(#NonNull final Context context, #NonNull final Uri contentUri, #NonNull final String bucketId)
{
try (final Cursor cursor = context.getContentResolver().query(contentUri,
null, MediaStore.Images.Media.BUCKET_ID + "=?", new String[]{bucketId}, null))
{
return ((cursor == null) || (cursor.moveToFirst() == false)) ? 0 : cursor.getCount();
}
}
public static final class Album
{
#NonNull
public final String buckedId;
#NonNull
public final String bucketName;
public final int count;
Album(#NonNull final String bucketId, #NonNull final String bucketName, final int count)
{
this.buckedId = bucketId;
this.bucketName = bucketName;
this.count = count;
}
}
}
This is a more efficient(not perfect) way to do that.
I am doing it for videos, but doing so is the same for images to. just change MediaStore.Video.Media.X to MediaStore.Images.Media.X
public class QUtils {
/*created by Nasib June 6, 2020*/
#RequiresApi(api = Build.VERSION_CODES.Q)
public static ArrayList<FolderHolder> loadListOfFolders(Context context) {
ArrayList<FolderHolder> allFolders = new ArrayList<>();//list that we need
HashMap<Long, String> folders = new HashMap<>(); //hashmap to track(no duplicates) folders by using their ids
String[] projection = {MediaStore.Video.Media._ID,
MediaStore.Video.Media.BUCKET_ID,
MediaStore.Video.Media.BUCKET_DISPLAY_NAME,
MediaStore.Video.Media.DATE_ADDED};
ContentResolver CR = context.getContentResolver();
Uri root = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL);
Cursor c = CR.query(root, projection, null, null, MediaStore.Video.Media.DATE_ADDED + " desc");
if (c != null && c.moveToFirst()) {
int folderIdIndex = c.getColumnIndexOrThrow(MediaStore.Video.Media.BUCKET_ID);
int folderNameIndex = c.getColumnIndexOrThrow(MediaStore.Video.Media.BUCKET_DISPLAY_NAME);
int thumbIdIndex = c.getColumnIndexOrThrow(MediaStore.Video.Media._ID);
int dateAddedIndex = c.getColumnIndexOrThrow(MediaStore.Video.Media.DATE_ADDED);
do {
Long folderId = c.getLong(folderIdIndex);
if (folders.containsKey(folderId) == false) { //proceed only if the folder data has not been inserted already :)
long thumbId = c.getLong(thumbIdIndex);
String folderName = c.getString(folderNameIndex);
String dateAdded = c.getString(dateAddedIndex);
Uri thumbPath = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, thumbId);
folders.put(folderId, folderName);
allFolders.add(new FolderHolder(String.valueOf(thumbPath), folderName, dateAdded));
}
} while (c.moveToNext());
c.close(); //close cursor
folders.clear(); //clear the hashmap becuase it's no more useful
}
return allFolders;
}
}
FolderHolder model class
public class FolderHolder {
private String folderName;
public long dateAdded;
private String thumbnailPath;
public long folderId;
public void setPath(String thumbnailPath) {
this.thumbnailPath = thumbnailPath;
}
public String getthumbnailPath() {
return thumbnailPath;
}
public FolderHolder(long folderId, String thumbnailPath, String folderName, long dateAdded) {
this.folderId = folderId;
this.folderName = folderName;
this.thumbnailPath = thumbnailPath;
this.dateAdded = dateAdded;
}
public String getFolderName() {
return folderName;
}
}
GROUP_BY supporting in case of using Bundle:
val bundle = Bundle().apply {
putString(
ContentResolver.QUERY_ARG_SQL_SORT_ORDER,
"${MediaStore.MediaColumns.DATE_MODIFIED} DESC"
)
putString(
ContentResolver.QUERY_ARG_SQL_GROUP_BY,
MediaStore.Images.ImageColumns.BUCKET_ID
)
}
contentResolver.query(
uri,
arrayOf(
MediaStore.Images.ImageColumns.BUCKET_ID,
MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME,
MediaStore.Images.ImageColumns.DATE_TAKEN,
MediaStore.Images.ImageColumns.DATA
),
bundle,
null
)
I am a decades-old C programmer. Unity is my first foray into modern C and I find myself pleasantly surprised - but some of the subtleties of the language have escaped me.
First a technical question: What is the right way to store a Unity Object in a generic class? In the example below I get the following error:
Assets/scripts/MediaTest.cs(49,44): error CS0030: Cannot convert type `UnityEngine.Texture2D' to `T'
Second the real question: What is a better approach to loading a set of textures?
Thanks in advance for your help
/*
* MediaTest.cs
*
* Pared down generic class test
*
*/
using System.Collections; // Load IEnumerable
using System.Collections.Generic; // Load Dictionary
using System.Text.RegularExpressions; // Load Regex
using UnityEngine; // Load UnityEngine.Object
public class MediaTest<T> : IEnumerable where T : UnityEngine.Object {
private Dictionary<string, MediaContent> _dict =
new Dictionary<string, MediaContent>();
private class MediaContent {
public string path { get; set; }
public T item { get; set; }
public MediaContent(string path, T item) {
this.path = path;
this.item = item;
}
}
// Indexer to return a UnityEngine.Object by filename
public T this[string name] {
get { return (T)_dict[name].item; }
}
// Convert a path to just the filename
public string Basename(string path) {
return new Regex(#"^.*/").Replace(path, "");
}
// Iterate through the filenames (keys) to the stored objects
public IEnumerator GetEnumerator() {
foreach (string name in _dict.Keys) {
yield return name;
}
}
// Read in the Resource at the specified path into a UnityEngine.Object
public void Load(string path, bool load=false) {
string name = Basename(path);
if (this.GetType() == typeof(Media<Texture2D>) && IsStill(name)) {
T item = (load) ? (T)Resources.Load<Texture2D>(path) : null;
_dict[name] = new MediaContent(path, item);
return;
}
if (this.GetType() == typeof(Media<AudioClip>) && IsAudio(name)) {
T item = (load) ? (T)Resources.Load<AudioClip>(path) : null;
_dict[name] = new MediaContent(path, item);
return;
}
}
// The real code uses Regex.Match on the file extension for supported types
public bool IsStill(string name) { return true; }
public bool IsAudio(string name) { return true; }
}
Here is the working code with updates from the comments. Thanks derHugo!
public void Load(string path, bool load=false) {
// Translate the filesystem path to a Resources relative path by removing both
// The */Resources prefix and the filename extention
Regex re_path = new Regex(#".*/Resources/");
Regex re_ext = new Regex(#"\.\w+$");
string relpath = re_ext.Replace(re_path.Replace(path, ""), "");
// Create an asset name from the path
string name = System.IO.Path.GetFileName(relpath);
// Skip this file if it doesn't match our type
if ( (typeof(T) == typeof(Texture2D) && IsStill(path)) ||
// (typeof(T) == typeof(Video) && IsVideo(path)) ||
(typeof(T) == typeof(AudioClip) && IsAudio(name))) {
T item = (load) ? Resources.Load(relpath) as T : null;
_dict[name] = new MediaContent(path, item);
}
}
I have seen that you can configure routing in ASP.NET Core 2.0 to generate lower case urls as described here: https://stackoverflow.com/a/45777372/83825
Using this:
services.AddRouting(options => options.LowercaseUrls = true);
However, although this is fine to GENERATE the urls, it doesn't appear to do anything to actually ENFORCE them, that is, redirect any urls that are NOT all lowercase to the corresponding lowercase url (preferably via 301 redirect).
I know people are accessing my site via differently cased urls, and I want them to all be lowercase, permanently.
Is doing a standard redirect via RewriteOptions and Regex the only way to do this? what would be the appropriate expression to do this:
var options = new RewriteOptions().AddRedirect("???", "????");
Or is there another way?
I appreciate this is many months old, however for people who may be looking for the same solution, you can add a complex redirect implementing IRule such as:
public class RedirectLowerCaseRule : IRule
{
public int StatusCode { get; } = (int)HttpStatusCode.MovedPermanently;
public void ApplyRule(RewriteContext context)
{
HttpRequest request = context.HttpContext.Request;
PathString path = context.HttpContext.Request.Path;
HostString host = context.HttpContext.Request.Host;
if (path.HasValue && path.Value.Any(char.IsUpper) || host.HasValue && host.Value.Any(char.IsUpper))
{
HttpResponse response = context.HttpContext.Response;
response.StatusCode = StatusCode;
response.Headers[HeaderNames.Location] = (request.Scheme + "://" + host.Value + request.PathBase.Value + request.Path.Value).ToLower() + request.QueryString;
context.Result = RuleResult.EndResponse;
}
else
{
context.Result = RuleResult.ContinueRules;
}
}
}
This can then be applied in your Startup.cs under Configure method as such:
new RewriteOptions().Add(new RedirectLowerCaseRule());
Slightly different implementation, also inspired from this other thread.
public class RedirectLowerCaseRule : IRule
{
public void ApplyRule(RewriteContext context)
{
HttpRequest request = context.HttpContext.Request;
string url = request.Scheme + "://" + request.Host + request.PathBase + request.Path;
bool isGet = request.Method.ToLowerInvariant().Contains("get");
if ( isGet && url.Contains(".") == false && Regex.IsMatch(url, #"[A-Z]") )
{
HttpResponse response = context.HttpContext.Response;
response.Clear();
response.StatusCode = StatusCodes.Status301MovedPermanently;
response.Headers[HeaderNames.Location] = url.ToLowerInvariant() + request.QueryString;
context.Result = RuleResult.EndResponse;
}
else
{
context.Result = RuleResult.ContinueRules;
}
}
}
Changes made that I find useful:
Using ToLowerInvariant() instead of ToLower() (see possible issues here)
Keeping the port number in place.
Bypassing request methods other than GET.
Bypassing requests with a dot, assuming static files like js/css/images etc should keep any uppercase in place.
Using Microsoft.AspNetCore.Http.StatusCodes.
Adding as answer because I can't comment (yet). This is an addition to Ben Maxfields answer.
Using his code http://www.example.org/Example/example would NOT be redirected, since PathBase was not checked for uppercase letters (even though it was used to build the new lowercase URI).
So based on his code, I ended up using this:
public class RedirectLowerCaseRule : IRule
{
public int StatusCode { get; } = (int)HttpStatusCode.MovedPermanently;
public void ApplyRule(RewriteContext context)
{
HttpRequest request = context.HttpContext.Request;
PathString path = context.HttpContext.Request.Path;
PathString pathbase = context.HttpContext.Request.PathBase;
HostString host = context.HttpContext.Request.Host;
if ((path.HasValue && path.Value.Any(char.IsUpper)) || (host.HasValue && host.Value.Any(char.IsUpper)) || (pathbase.HasValue && pathbase.Value.Any(char.IsUpper)))
{
Console.WriteLine("Redirect should happen");
HttpResponse response = context.HttpContext.Response;
response.StatusCode = StatusCode;
response.Headers[HeaderNames.Location] = (request.Scheme + "://" + host.Value + request.PathBase + request.Path).ToLower() + request.QueryString;
context.Result = RuleResult.EndResponse;
}
else
{
context.Result = RuleResult.ContinueRules;
}
}
}
My two cents... based on https://github.com/aspnet/AspNetCore/blob/master/src/Middleware/Rewrite/src/RedirectToWwwRule.cs
public class RedirectToLowercaseRule : IRule
{
private readonly int _statusCode;
public RedirectToLowercaseRule(int statusCode)
{
_statusCode = statusCode;
}
public void ApplyRule(RewriteContext context)
{
var req = context.HttpContext.Request;
if (!req.Scheme.Any(char.IsUpper)
&& !req.Host.Value.Any(char.IsUpper)
&& !req.PathBase.Value.Any(char.IsUpper)
&& !req.Path.Value.Any(char.IsUpper))
{
context.Result = RuleResult.ContinueRules;
return;
}
var newUrl = UriHelper.BuildAbsolute(req.Scheme.ToLowerInvariant(), new HostString(req.Host.Value.ToLowerInvariant()), req.PathBase.Value.ToLowerInvariant(), req.Path.Value.ToLowerInvariant(), req.QueryString);
var response = context.HttpContext.Response;
response.StatusCode = _statusCode;
response.Headers[HeaderNames.Location] = newUrl;
context.Result = RuleResult.EndResponse;
context.Logger.RedirectedToLowercase();
}
}
With extension methods:
public static class RewriteOptionsExtensions
{
public static RewriteOptions AddRedirectToLowercase(this RewriteOptions options, int statusCode)
{
options.Add(new RedirectToLowercaseRule(statusCode));
return options;
}
public static RewriteOptions AddRedirectToLowercase(this RewriteOptions options)
{
return AddRedirectToLowercase(options, StatusCodes.Status307TemporaryRedirect);
}
public static RewriteOptions AddRedirectToLowercasePermanent(this RewriteOptions options)
{
return AddRedirectToLowercase(options, StatusCodes.Status308PermanentRedirect);
}
}
And logging:
internal static class MiddlewareLoggingExtensions
{
private static readonly Action<ILogger, Exception> _redirectedToLowercase = LoggerMessage.Define(LogLevel.Information, new EventId(1, "RedirectedToLowercase"), "Request redirected to lowercase");
public static void RedirectedToLowercase(this ILogger logger)
{
_redirectedToLowercase(logger, null);
}
}
And usage:
app.UseRewriter(new RewriteOptions()
.AddRedirectToLowercase());
Other considerations are the choice of status codes. I've used 307 and 308 in the extension methods as these prevent the request method being changed (e.g. from GET to POST) during the request, however if you want to allow that behaviour you can use 301 and 302. See What's the difference between HTTP 301 and 308 status codes? for further information.
Are you sure you want a redirect? If not, and your goal is that there is no such thing as uppercase in your host and path, you can use the following IRule. This assures me that wherever I look at the path in the pipeline that it is lowercase.
public class RewriteLowerCaseRule : IRule
{
public void ApplyRule(RewriteContext context)
{
var request = context.HttpContext.Request;
var host = request.Host;
var pathBase = request.PathBase;
var path = request.Path;
if (host.HasValue)
{
if (host.Port == null)
{
request.Host = new HostString(host.Host.ToLower());
}
else
{
request.Host = new HostString(host.Host.ToLower(), (int) host.Port);
}
}
if (pathBase.HasValue)
{
request.PathBase = new PathString(pathBase.Value.ToLower());
}
if (path.HasValue)
{
request.Path = new PathString(path.Value.ToLower());
request.PathBase = new PathString(pathBase.Value.ToLower());
}
context.Result = RuleResult.ContinueRules;
}
}
Usage:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseRewriter(new RewriteOptions().Add(new RewriteLowerCaseRule()));
...
}
I want to send a message to my Azure Service Bus Queue in .Net Core but the WindowsAzure.ServiceBus Package is not compatible with .Net Core.
Can anyone show me how to send a message to the queue using the REST API?
While the current client is not .NET Core compatible, the new client, that is a work in progress, is 100% compatible. The pre-release package will be available on April 3rd and the status can be tracked here. You could pull down the course code and compile it already today with the caveat that API will be changing as the team is trying to flesh out the design details.
Can anyone show me how to send a message to the queue using the REST API?
As 4c74356b41 mentioned in his comment, we could send a message to Azure Service Bus queue via this REST API:
POST http{s}://{serviceNamespace}.servicebus.windows.net/{queuePath|topicPath}/messages
here is a example
In above request, I provide a Shared Access Signature (token), to generate a Shared Access Signature (token), please refer to this article.
Thanks to Fred's answer, I've expanded to include how to post the authentication header with signature.
public class AzureServiceBusSettings
{
public string BaseUrl { get; set; }
public string SharedAccessKey { get; set; }
public string SharedAccessKeyName { get; set; }
}
public interface IServiceBus
{
/// <summary>
/// Publish domain events to domain topic.
/// </summary>
Task PublishAsync<T>(T #event)
/// <summary>
/// Send commands to command queue.
/// </summary>
Task SendAsync<T>(T command)
}
public class ServiceBus : IServiceBus
{
private readonly AzureServiceBusSettings _settings;
public ServiceBus(IOptions<AzureServiceBusSettings> azureServiceBusSettings)
{
_settings = azureServiceBusSettings.Value;
}
/// <summary>
/// Publish domain events to domain topic.
/// </summary>
public async Task PublishAsync<T>(T #event)
{
await SendInternalAsync(#event, "domain");
}
/// <summary>
/// Send commands to command queue.
/// </summary>
public async Task SendAsync<T>(T command)
{
await SendInternalAsync(command, "commands");
}
private async Task SendInternalAsync<T>(T command, string queueName)
{
var json = JsonConvert.SerializeObject(command);
var content = new StringContent(json, Encoding.UTF8, "application/json");
using (var httpClient = new HttpClient())
{
httpClient.BaseAddress = new Uri(_settings.BaseUrl);
try
{
var url = $"/{queueName}/messages";
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("SharedAccessSignature", GetSasToken(queueName));
var response = await httpClient.PostAsync(url, content);
// Success returns 201 Created.
if (!response.IsSuccessStatusCode)
{
// Handle this.
}
}
catch (Exception ex)
{
// Handle this.
// throw;
}
}
}
private string GetSasToken(string queueName)
{
var url = $"{_settings.BaseUrl}/{queueName}";
// Expiry minutes should be a setting.
var expiry = (int)DateTime.UtcNow.AddMinutes(20).Subtract(new DateTime(1970, 1, 1)).TotalSeconds;
var signature = GetSignature(url, _settings.SharedAccessKey);
var token = $"sr={WebUtility.UrlEncode(url)}&sig={WebUtility.UrlEncode(signature)}&se={expiry}&skn={_settings.SharedAccessKeyName}";
return token;
}
private static string GetSignature(string url, string key)
{
var expiry = (int)DateTime.UtcNow.AddMinutes(20).Subtract(new DateTime(1970, 1, 1)).TotalSeconds;
var value = WebUtility.UrlEncode(url) + "\n" + expiry;
var encoding = new UTF8Encoding();
var keyByte = encoding.GetBytes(key);
var valueBytes = encoding.GetBytes(value);
using (var hmacsha256 = new HMACSHA256(keyByte))
{
var hashmessage = hmacsha256.ComputeHash(valueBytes);
var result = Convert.ToBase64String(hashmessage);
return result;
}
}
}
And a simple xunit test to post:
public class ServiceBusTests
{
public class FooCommand : ICommand
{
public Guid CommandId { get; set; }
}
private Mock<IOptions<AzureServiceBusSettings>> _mockAzureServiceBusOptions;
private ServiceBus _sut;
public ServiceBusTests()
{
var settings = new AzureServiceBusSettings
{
BaseUrl = "https://my-domain.servicebus.windows.net",
SharedAccessKey = "my-key-goes-here",
SharedAccessKeyName = "RootManageSharedAccessKey"
};
_mockAzureServiceBusOptions = new Mock<IOptions<AzureServiceBusSettings>>();
_mockAzureServiceBusOptions.SetupGet(o => o.Value).Returns(settings);
_sut = new ServiceBus(
_mockAzureServiceBusOptions.Object);
}
[Fact]
public async Task should_send_message()
{
// Arrange.
var command = new FooCommand {CommandId = Guid.NewGuid()};
// Act.
await _sut.SendAsync(command);
// Assert.
// TODO: Get the command from the queue and assert something.
}
}